From 93184d21d13d86c6cfa585978cf8c7f8458c1015 Mon Sep 17 00:00:00 2001 From: Fr4nzD13trich Date: Thu, 18 Sep 2025 18:11:17 +0200 Subject: [PATCH] repo created --- CHANGELOG.md | 659 + CODE_OF_CONDUCT.md | 13 + CONTRIBUTING.md | 412 + LICENCE.md | 679 + LICENSES/AGPL-3.0-or-later.txt | 235 + LICENSES/Apache-2.0.txt | 73 + LICENSES/CC-BY-4.0.txt | 156 + LICENSES/CC-BY-SA-3.0.txt | 99 + LICENSES/CC0-1.0.txt | 121 + LICENSES/GPL-2.0-only.txt | 117 + LICENSES/GPL-3.0-or-later.txt | 232 + LICENSES/LGPL-2.1-or-later.txt | 175 + LICENSES/LGPL-3.0-or-later.txt | 304 + LICENSES/LicenseRef-NextcloudTrademarks.txt | 9 + LICENSES/LicenseRef-XTrademarks.txt | 49 + LICENSES/MIT.txt | 9 + README.md | 115 + REUSE.toml | 36 + SECURITY.md | 28 + SETUP.md | 132 + app/build.gradle | 418 + app/lint.xml | 56 + app/proguard-rules.pro | 27 + .../10.json | 146 + .../11.json | 719 + .../12.json | 725 + .../13.json | 749 + .../14.json | 719 + .../15.json | 725 + .../16.json | 731 + .../17.json | 730 + .../18.json | 746 + .../19.json | 746 + .../20.json | 751 + .../21.json | 761 + .../8.json | 138 + .../9.json | 139 + .../talk/ExampleInstrumentedTest.java | 35 + .../talk/activities/MainActivityTest.kt | 48 + .../data/database/dao/ChatBlocksDaoTest.kt | 393 + .../data/database/dao/ChatMessagesDaoTest.kt | 276 + .../database/migrations/MigrationsTest.kt | 114 + .../java/com/nextcloud/talk/ui/LoginIT.java | 132 + .../talk/utils/ColorGeneratorTest.kt | 68 + .../com/nextcloud/talk/utils/ShareUtilsIT.kt | 55 + .../com/nextcloud/talk/utils/UriUtilsIT.kt | 142 + .../talk/utils/VibrationUtilsTest.kt | 45 + .../android/tr-TR/full_description.txt | 27 + .../talk/utils/ClosedInterfaceImpl.java | 28 + app/src/gplay/AndroidManifest.xml | 36 + .../talk/jobs/GetFirebasePushTokenWorker.kt | 63 + .../firebase/NCFirebaseMessagingService.kt | 81 + .../talk/utils/ClosedInterfaceImpl.kt | 93 + app/src/main/AndroidManifest.xml | 332 + .../main/assets/leafletMapMessagePreview.html | 42 + .../leafletMapMessagePreview.html.license | 2 + app/src/main/ic_launcher-web.png | Bin 0 -> 25403 bytes .../java/com/nextcloud/talk/PhoneUtils.kt | 12 + .../account/AccountVerificationActivity.kt | 521 + .../talk/account/ServerSelectionActivity.kt | 468 + .../talk/account/SwitchAccountActivity.kt | 181 + .../talk/account/WebViewLoginActivity.kt | 471 + .../talk/account/data/LoginRepository.kt | 161 + .../account/data/io/LocalLoginDataSource.kt | 45 + .../account/data/model/LoginResponseModels.kt | 12 + .../data/network/NetworkLoginDataSource.kt | 122 + .../BrowserLoginActivityViewModel.kt | 92 + .../talk/activities/ActionBarProvider.java | 13 + .../nextcloud/talk/activities/BaseActivity.kt | 295 + .../nextcloud/talk/activities/CallActivity.kt | 3355 +++ .../talk/activities/CallBaseActivity.java | 149 + .../nextcloud/talk/activities/CallStatus.kt | 22 + .../nextcloud/talk/activities/MainActivity.kt | 289 + .../talk/activities/TakePhotoActivity.java | 423 + .../talk/adapters/GeocodingAdapter.kt | 59 + .../talk/adapters/ParticipantDisplayItem.kt | 215 + .../ParticipantDisplayItemNotifier.java | 40 + .../adapters/PredefinedStatusClickListener.kt | 14 + .../adapters/PredefinedStatusListAdapter.kt | 34 + .../adapters/PredefinedStatusViewHolder.kt | 60 + .../nextcloud/talk/adapters/ReactionItem.kt | 11 + .../adapters/ReactionItemClickListener.kt | 11 + .../talk/adapters/ReactionsAdapter.kt | 29 + .../talk/adapters/ReactionsViewHolder.kt | 48 + .../talk/adapters/items/AdvancedUserItem.kt | 109 + .../talk/adapters/items/ContactItem.kt | 192 + .../talk/adapters/items/ConversationItem.kt | 507 + .../adapters/items/FlexibleItemViewType.kt | 17 + .../adapters/items/GenericTextHeaderItem.kt | 72 + .../adapters/items/LoadMoreResultsItem.kt | 53 + .../adapters/items/MentionAutocompleteItem.kt | 264 + .../talk/adapters/items/MessageResultItem.kt | 89 + .../adapters/items/MessagesTextHeaderItem.kt | 20 + .../adapters/items/NotificationSoundItem.java | 95 + .../talk/adapters/items/ParticipantItem.kt | 320 + .../talk/adapters/items/SpacerItem.kt | 37 + .../AdjustableMessageHolderInterface.kt | 51 + .../messages/CallStartedMessageInterface.kt | 12 + .../messages/CommonMessageInterface.kt | 16 + .../messages/IncomingDeckCardViewHolder.kt | 260 + .../IncomingLinkPreviewMessageViewHolder.kt | 252 + .../IncomingLocationMessageViewHolder.kt | 286 + .../messages/IncomingPollMessageViewHolder.kt | 254 + .../IncomingPreviewMessageViewHolder.java | 145 + .../messages/IncomingTextMessageViewHolder.kt | 428 + .../IncomingVoiceMessageViewHolder.kt | 390 + .../talk/adapters/messages/LinkPreview.kt | 119 + .../talk/adapters/messages/MessagePayload.kt | 15 + .../messages/OutcomingDeckCardViewHolder.kt | 252 + .../OutcomingLinkPreviewMessageViewHolder.kt | 243 + .../OutcomingLocationMessageViewHolder.kt | 292 + .../OutcomingPollMessageViewHolder.kt | 252 + .../OutcomingPreviewMessageViewHolder.java | 143 + .../OutcomingTextMessageViewHolder.kt | 452 + .../OutcomingVoiceMessageViewHolder.kt | 398 + .../messages/PreviewMessageInterface.kt | 13 + .../messages/PreviewMessageViewHolder.kt | 359 + .../talk/adapters/messages/Reaction.kt | 181 + .../messages/SystemMessageInterface.kt | 14 + .../messages/SystemMessageViewHolder.kt | 194 + .../messages/TalkMessagesListAdapter.java | 86 + .../talk/adapters/messages/Thread.kt | 45 + .../talk/adapters/messages/ThreadButton.kt | 88 + .../UnreadNoticeMessageViewHolder.java | 37 + .../messages/VoiceMessageInterface.kt | 16 + .../java/com/nextcloud/talk/api/NcApi.java | 649 + .../com/nextcloud/talk/api/NcApiCoroutines.kt | 309 + .../application/NextcloudTalkApplication.kt | 252 + .../ArbitraryStorageManager.kt | 23 + .../items/BasicListItemWithImage.kt | 22 + .../talk/bottomsheet/items/BottomSheets.kt | 63 + .../items/ListIconDialogAdapter.kt | 131 + .../nextcloud/talk/call/CallParticipant.java | 219 + .../talk/call/CallParticipantList.java | 154 + .../call/CallParticipantListNotifier.java | 50 + .../talk/call/CallParticipantModel.java | 190 + .../call/CallParticipantModelNotifier.java | 85 + .../talk/call/LocalCallParticipantModel.java | 114 + .../LocalCallParticipantModelNotifier.java | 73 + .../talk/call/LocalStateBroadcaster.java | 170 + .../talk/call/LocalStateBroadcasterMcu.java | 118 + .../talk/call/LocalStateBroadcasterNoMcu.java | 128 + .../nextcloud/talk/call/MessageSender.java | 93 + .../nextcloud/talk/call/MessageSenderMcu.java | 41 + .../talk/call/MessageSenderNoMcu.java | 47 + .../call/MutableCallParticipantModel.java | 73 + .../MutableLocalCallParticipantModel.java | 51 + .../com/nextcloud/talk/call/RaisedHand.kt | 9 + .../nextcloud/talk/call/ReactionAnimator.kt | 166 + .../call/components/AvatarWithFallback.kt | 62 + .../talk/call/components/ParticipantGrid.kt | 277 + .../talk/call/components/ParticipantTile.kt | 146 + .../talk/call/components/WebRTCVideoView.kt | 35 + .../MentionAutocompleteCallback.java | 91 + .../CallNotificationActivity.kt | 244 + .../com/nextcloud/talk/chat/ChatActivity.kt | 4602 ++++ .../talk/chat/MessageInputFragment.kt | 1126 + .../MessageInputVoiceRecordingFragment.kt | 228 + .../com/nextcloud/talk/chat/OverflowMenu.kt | 160 + .../nextcloud/talk/chat/TypingParticipant.kt | 45 + .../talk/chat/data/ChatMessageRepository.kt | 119 + .../chat/data/io/AudioFocusRequestManager.kt | 102 + .../talk/chat/data/io/AudioRecorderManager.kt | 143 + .../chat/data/io/LifecycleAwareManager.kt | 32 + .../talk/chat/data/io/MediaPlayerManager.kt | 350 + .../talk/chat/data/io/MediaRecorderManager.kt | 171 + .../talk/chat/data/model/ChatMessage.kt | 446 + .../data/network/ChatNetworkDataSource.kt | 81 + .../network/OfflineFirstChatRepository.kt | 1109 + .../chat/data/network/RetrofitChatNetwork.kt | 224 + .../talk/chat/viewmodels/ChatViewModel.kt | 1012 + .../chat/viewmodels/MessageInputViewModel.kt | 280 + .../talk/components/ColoredStatusBar.kt | 60 + .../talk/components/StandardAppBar.kt | 88 + .../talk/components/VerticallyCenteredRow.kt | 24 + .../talk/contacts/ContactsActivity.kt | 83 + .../talk/contacts/ContactsApplication.kt | 38 + .../talk/contacts/ContactsRepository.kt | 22 + .../talk/contacts/ContactsRepositoryImpl.kt | 99 + .../nextcloud/talk/contacts/ContactsScreen.kt | 83 + .../talk/contacts/ContactsViewModel.kt | 157 + .../nextcloud/talk/contacts/ImageRequest.kt | 38 + .../com/nextcloud/talk/contacts/ShareType.kt | 16 + .../contacts/components/ContactItemRow.kt | 120 + .../contacts/components/ContactsAppBar.kt | 77 + .../talk/contacts/components/ContactsItem.kt | 74 + .../talk/contacts/components/ContactsList.kt | 59 + .../components/ContactsSearchAppBar.kt | 117 + .../components/ConversationCreationOptions.kt | 92 + .../talk/contacts/components/Header.kt | 34 + .../RenameConversationDialogFragment.kt | 227 + .../ConversationCreationActivity.kt | 731 + .../ConversationCreationRepository.kt | 28 + .../ConversationCreationRepositoryImpl.kt | 177 + .../ConversationCreationViewModel.kt | 162 + .../ConversationInfoActivity.kt | 1953 ++ .../conversationinfo/CreateRoomRequest.kt | 72 + .../conversationinfo/GuestAccessHelper.kt | 197 + .../talk/conversationinfo/Participants.kt | 27 + .../viewmodel/ConversationInfoViewModel.kt | 540 + .../ConversationInfoEditActivity.kt | 349 + .../data/ConversationInfoEditRepository.kt | 24 + .../ConversationInfoEditRepositoryImpl.kt | 88 + .../ConversationInfoEditViewModel.kt | 178 + .../ConversationsListActivity.kt | 2279 ++ .../data/OfflineConversationsRepository.kt | 43 + .../network/ConversationsNetworkDataSource.kt | 16 + .../OfflineFirstConversationsRepository.kt | 170 + .../network/RetrofitConversationsNetwork.kt | 28 + .../viewmodels/ConversationsListViewModel.kt | 228 + .../talk/dagger/modules/BusModule.java | 24 + .../talk/dagger/modules/ContextModule.java | 27 + .../talk/dagger/modules/DaosModule.kt | 27 + .../talk/dagger/modules/DatabaseModule.java | 54 + .../talk/dagger/modules/ManagerModule.kt | 36 + .../talk/dagger/modules/RepositoryModule.kt | 218 + .../talk/dagger/modules/RestModule.java | 328 + .../talk/dagger/modules/UtilsModule.kt | 32 + .../talk/dagger/modules/ViewModelModule.kt | 169 + .../talk/data/database/dao/ChatBlocksDao.kt | 84 + .../talk/data/database/dao/ChatMessagesDao.kt | 224 + .../data/database/dao/ConversationsDao.kt | 66 + .../database/mappers/ChatMessageMapUtils.kt | 113 + .../database/mappers/ConversationMapUtils.kt | 176 + .../data/database/model/ChatBlockEntity.kt | 42 + .../data/database/model/ChatMessageEntity.kt | 74 + .../data/database/model/ConversationEntity.kt | 117 + .../talk/data/database/model/SendStatus.kt | 14 + .../talk/data/network/NetworkMonitor.kt | 27 + .../talk/data/network/NetworkMonitorImpl.kt | 84 + .../talk/data/source/local/Migrations.kt | 421 + .../talk/data/source/local/TalkDatabase.kt | 143 + .../local/converters/ArrayListConverter.kt | 39 + .../local/converters/CapabilitiesConverter.kt | 31 + .../ExternalSignalingServerConverter.kt | 31 + .../converters/HashMapHashMapConverter.kt | 31 + .../converters/LinkedHashMapConverter.kt | 58 + .../LinkedHashMapStringIntConverter.kt | 50 + .../converters/PushConfigurationConverter.kt | 32 + .../local/converters/SendStatusConverter.kt | 19 + .../converters/ServerVersionConverter.kt | 31 + .../converters/SignalingSettingsConverter.kt | 32 + .../data/storage/ArbitraryStorageMapper.kt | 30 + .../talk/data/storage/ArbitraryStoragesDao.kt | 40 + .../storage/ArbitraryStoragesRepository.kt | 18 + .../ArbitraryStoragesRepositoryImpl.kt | 31 + .../data/storage/model/ArbitraryStorage.kt | 18 + .../storage/model/ArbitraryStorageEntity.kt | 28 + .../nextcloud/talk/data/user/UserMapper.kt | 55 + .../com/nextcloud/talk/data/user/UsersDao.kt | 84 + .../talk/data/user/UsersRepository.kt | 32 + .../talk/data/user/UsersRepositoryImpl.kt | 85 + .../nextcloud/talk/data/user/model/User.kt | 50 + .../talk/data/user/model/UserEntity.kt | 63 + .../talk/diagnose/DiagnoseActivity.kt | 496 + .../diagnose/DiagnoseContentComposable.kt | 264 + .../talk/diagnose/DiagnoseViewModel.kt | 69 + .../talk/events/CertificateEvent.java | 42 + .../talk/events/ConfigurationChangeEvent.java | 34 + .../events/ConversationsListFetchDataEvent.kt | 9 + .../nextcloud/talk/events/EventStatus.java | 90 + .../talk/events/MoreMenuClickEvent.java | 54 + .../nextcloud/talk/events/NetworkEvent.java | 55 + .../talk/events/OpenConversationEvent.kt | 38 + .../talk/events/ProximitySensorEvent.java | 61 + .../talk/events/UserMentionClickEvent.java | 52 + .../events/WebSocketCommunicationEvent.java | 71 + .../talk/extensions/ImageViewExtensions.kt | 497 + .../talk/extensions/LongFormatExtension.kt | 14 + .../talk/extensions/ParcelableExtensions.kt | 30 + .../talk/filebrowser/models/BrowserFile.kt | 106 + .../talk/filebrowser/models/DavResponse.java | 69 + .../models/properties/NCEncrypted.kt | 44 + .../models/properties/NCPermission.kt | 45 + .../models/properties/NCPreview.kt | 44 + .../models/properties/OCFavorite.kt | 44 + .../filebrowser/models/properties/OCId.kt | 44 + .../filebrowser/models/properties/OCSize.kt | 44 + .../talk/filebrowser/webdav/DavUtils.java | 94 + .../webdav/ReadFilesystemOperation.java | 93 + .../webdav/ReadFolderListingOperation.kt | 166 + .../fullscreenfile/FullScreenImageActivity.kt | 198 + .../fullscreenfile/FullScreenMediaActivity.kt | 219 + .../FullScreenTextViewerActivity.kt | 124 + .../talk/interfaces/ClosedInterface.kt | 15 + .../interfaces/ConversationMenuInterface.kt | 14 + .../talk/invitation/InvitationsActivity.kt | 174 + .../invitation/adapters/InvitationsAdapter.kt | 86 + .../talk/invitation/data/Invitation.kt | 21 + .../invitation/data/InvitationActionModel.kt | 9 + .../talk/invitation/data/InvitationsModel.kt | 11 + .../invitation/data/InvitationsRepository.kt | 16 + .../data/InvitationsRepositoryImpl.kt | 72 + .../viewmodels/InvitationsViewModel.kt | 121 + .../talk/jobs/AccountRemovalWorker.java | 206 + .../AddParticipantsToConversationWorker.java | 127 + .../talk/jobs/CapabilitiesWorker.java | 157 + .../talk/jobs/ContactAddressBookWorker.kt | 505 + .../talk/jobs/DeleteConversationWorker.java | 113 + .../talk/jobs/DownloadFileToCacheWorker.kt | 156 + .../talk/jobs/LeaveConversationWorker.kt | 108 + .../nextcloud/talk/jobs/NotificationWorker.kt | 1018 + .../talk/jobs/PushRegistrationWorker.java | 75 + .../talk/jobs/SaveFileToStorageWorker.kt | 115 + .../talk/jobs/ShareOperationWorker.kt | 98 + .../talk/jobs/SignalingSettingsWorker.java | 149 + .../talk/jobs/UploadAndShareFilesWorker.kt | 365 + .../talk/jobs/WebsocketConnectionsWorker.java | 70 + .../talk/location/GeocodingActivity.kt | 223 + .../talk/location/GeocodingResult.kt | 13 + .../talk/location/LocationPickerActivity.kt | 589 + .../com/nextcloud/talk/lock/LockedActivity.kt | 150 + .../messagesearch/MessageSearchActivity.kt | 248 + .../talk/messagesearch/MessageSearchHelper.kt | 98 + .../messagesearch/MessageSearchViewModel.kt | 105 + .../talk/models/ExternalSignalingServer.kt | 28 + .../nextcloud/talk/models/ImportAccount.java | 94 + .../com/nextcloud/talk/models/LoginData.kt | 15 + .../com/nextcloud/talk/models/MessageDraft.kt | 57 + .../nextcloud/talk/models/RetrofitBucket.kt | 14 + .../nextcloud/talk/models/RingtoneSettings.kt | 27 + .../talk/models/SignatureVerification.kt | 15 + .../talk/models/TakePictureViewModel.java | 102 + .../talk/models/domain/ConversationModel.kt | 137 + .../talk/models/domain/ReactionAddedModel.kt | 11 + .../models/domain/ReactionDeletedModel.kt | 11 + .../talk/models/domain/SearchMessageEntry.kt | 17 + .../models/domain/StartCallRecordingModel.kt | 9 + .../models/domain/StopCallRecordingModel.kt | 9 + .../DomainEnumNotificationLevelConverter.kt | 39 + .../nextcloud/talk/models/json/AnyParceler.kt | 18 + .../json/autocomplete/AutocompleteOCS.kt | 27 + .../json/autocomplete/AutocompleteOverall.kt | 23 + .../json/autocomplete/AutocompleteUser.kt | 27 + .../models/json/capabilities/Capabilities.kt | 36 + .../json/capabilities/CapabilitiesList.kt | 25 + .../json/capabilities/CapabilitiesOCS.kt | 27 + .../json/capabilities/CapabilitiesOverall.kt | 23 + .../json/capabilities/CoreCapability.kt | 32 + .../capabilities/NotificationsCapability.kt | 25 + .../capabilities/ProvisioningCapability.kt | 26 + .../json/capabilities/RoomCapabilitiesOCS.kt | 27 + .../capabilities/RoomCapabilitiesOverall.kt | 23 + .../models/json/capabilities/ServerVersion.kt | 28 + .../json/capabilities/SpreedCapability.kt | 40 + .../json/capabilities/ThemingCapability.kt | 46 + .../json/capabilities/UserStatusCapability.kt | 29 + .../talk/models/json/chat/ChatMessageJson.kt | 56 + .../talk/models/json/chat/ChatOCS.kt | 26 + .../models/json/chat/ChatOCSSingleMessage.kt | 26 + .../talk/models/json/chat/ChatOverall.kt | 23 + .../json/chat/ChatOverallSingleMessage.kt | 23 + .../talk/models/json/chat/ChatShareOCS.kt | 23 + .../talk/models/json/chat/ChatShareOverall.kt | 23 + .../models/json/chat/ChatShareOverviewOCS.kt | 27 + .../json/chat/ChatShareOverviewOverall.kt | 23 + .../talk/models/json/chat/ChatUtils.kt | 61 + .../talk/models/json/chat/ReadStatus.kt | 13 + .../models/json/conversations/Conversation.kt | 175 + .../json/conversations/ConversationEnums.kt | 52 + .../talk/models/json/conversations/RoomOCS.kt | 26 + .../models/json/conversations/RoomOverall.kt | 23 + .../models/json/conversations/RoomsOCS.kt | 26 + .../models/json/conversations/RoomsOverall.kt | 23 + .../conversations/password/PasswordData.kt | 23 + .../conversations/password/PasswordOCS.kt | 27 + .../conversations/password/PasswordOverall.kt | 23 + .../ConversationObjectTypeConverter.kt | 41 + .../json/converters/EnumActorTypeConverter.kt | 50 + .../converters/EnumLobbyStateConverter.java | 37 + .../EnumNotificationLevelConverter.java | 46 + .../EnumParticipantTypeConverter.java | 54 + .../EnumReactionActorTypeConverter.kt | 34 + .../EnumReadOnlyConversationConverter.java | 38 + .../converters/EnumRoomTypeConverter.java | 54 + .../EnumSystemMessageTypeConverter.kt | 220 + .../LoganSquareJodaTimeConverter.java | 46 + .../converters/ObjectParcelConverter.java | 23 + .../json/converters/ScopeConverter.java | 33 + .../json/converters/UriTypeConverter.java | 31 + .../talk/models/json/generic/GenericMeta.kt | 27 + .../talk/models/json/generic/GenericOCS.kt | 23 + .../models/json/generic/GenericOverall.kt | 23 + .../talk/models/json/generic/Status.kt | 41 + .../talk/models/json/hovercard/HoverCard.kt | 26 + .../models/json/hovercard/HoverCardAction.kt | 28 + .../models/json/hovercard/HoverCardOCS.kt | 26 + .../models/json/hovercard/HoverCardOverall.kt | 19 + .../talk/models/json/invitation/Invitation.kt | 43 + .../models/json/invitation/InvitationOCS.kt | 25 + .../json/invitation/InvitationOverall.kt | 22 + .../talk/models/json/mention/Mention.kt | 39 + .../talk/models/json/mention/MentionOCS.kt | 27 + .../models/json/mention/MentionOverall.kt | 23 + .../models/json/notifications/Notification.kt | 53 + .../json/notifications/NotificationAction.kt | 29 + .../json/notifications/NotificationOCS.kt | 26 + .../json/notifications/NotificationOverall.kt | 22 + .../notifications/NotificationRichObject.kt | 27 + .../json/notifications/NotificationsOCS.kt | 27 + .../notifications/NotificationsOverall.kt | 20 + .../models/json/opengraph/OpenGraphOCS.kt | 25 + .../models/json/opengraph/OpenGraphObject.kt | 30 + .../models/json/opengraph/OpenGraphOverall.kt | 22 + .../json/opengraph/OpenGraphResponse.kt | 22 + .../talk/models/json/opengraph/Reference.kt | 28 + .../talk/models/json/opengraph/RichObject.kt | 30 + .../json/participants/AddParticipantOCS.kt | 28 + .../participants/AddParticipantOverall.kt | 24 + .../models/json/participants/Participant.kt | 141 + .../json/participants/ParticipantsOCS.kt | 26 + .../json/participants/ParticipantsOverall.kt | 23 + .../talk/models/json/participants/TalkBan.kt | 40 + .../models/json/participants/TalkBanOCS.kt | 26 + .../json/participants/TalkBanOverall.kt | 23 + .../models/json/profile/CoreProfileAction.kt | 29 + .../talk/models/json/profile/Profile.kt | 30 + .../talk/models/json/profile/ProfileOCS.kt | 26 + .../models/json/profile/ProfileOverall.kt | 23 + .../models/json/push/DecryptedPushMessage.kt | 106 + .../talk/models/json/push/NotificationUser.kt | 29 + .../json/push/PushConfigurationState.kt | 35 + .../talk/models/json/push/PushRegistration.kt | 29 + .../models/json/push/PushRegistrationOCS.kt | 26 + .../json/push/PushRegistrationOverall.kt | 23 + .../models/json/reactions/ReactionVoter.kt | 35 + .../models/json/reactions/ReactionsOCS.kt | 27 + .../models/json/reactions/ReactionsOverall.kt | 22 + .../talk/models/json/reminder/Reminder.kt | 28 + .../talk/models/json/reminder/ReminderOCS.kt | 25 + .../models/json/reminder/ReminderOverall.kt | 22 + .../models/json/search/ContactsByNumberOCS.kt | 26 + .../json/search/ContactsByNumberOverall.kt | 23 + .../talk/models/json/sharees/ExactSharees.kt | 23 + .../talk/models/json/sharees/Sharee.kt | 27 + .../talk/models/json/sharees/ShareesOCS.kt | 26 + .../models/json/sharees/ShareesOverall.kt | 23 + .../talk/models/json/sharees/SharesData.kt | 25 + .../talk/models/json/sharees/Value.kt | 23 + .../json/signaling/DataChannelMessage.kt | 34 + .../models/json/signaling/NCIceCandidate.kt | 27 + .../models/json/signaling/NCMessagePayload.kt | 37 + .../json/signaling/NCSignalingMessage.kt | 35 + .../talk/models/json/signaling/Signaling.kt | 29 + .../models/json/signaling/SignalingOCS.kt | 26 + .../models/json/signaling/SignalingOverall.kt | 23 + .../settings/FederationHelloAuthParams.kt | 24 + .../signaling/settings/FederationSettings.kt | 30 + .../json/signaling/settings/IceServer.kt | 32 + .../signaling/settings/SignalingSettings.kt | 33 + .../settings/SignalingSettingsOcs.kt | 26 + .../settings/SignalingSettingsOverall.kt | 23 + .../talk/models/json/status/ClearAt.kt | 25 + .../talk/models/json/status/Status.kt | 39 + .../talk/models/json/status/StatusOCS.kt | 26 + .../talk/models/json/status/StatusOverall.kt | 23 + .../talk/models/json/status/StatusType.kt | 16 + .../status/predefined/PredefinedStatus.kt | 29 + .../status/predefined/PredefinedStatusOCS.kt | 26 + .../predefined/PredefinedStatusOverall.kt | 23 + .../testNotification/TestNotificationData.kt | 24 + .../testNotification/TestNotificationOCS.kt | 26 + .../TestNotificationOverall.kt | 23 + .../talk/models/json/threads/Thread.kt | 34 + .../models/json/threads/ThreadAttendee.kt | 19 + .../talk/models/json/threads/ThreadInfo.kt | 29 + .../talk/models/json/threads/ThreadOCS.kt | 25 + .../talk/models/json/threads/ThreadOverall.kt | 22 + .../talk/models/json/threads/ThreadsOCS.kt | 25 + .../models/json/threads/ThreadsOverall.kt | 22 + .../json/unifiedsearch/UnifiedSearchEntry.kt | 34 + .../json/unifiedsearch/UnifiedSearchOCS.kt | 25 + .../unifiedsearch/UnifiedSearchOverall.kt | 22 + .../UnifiedSearchResponseData.kt | 29 + .../json/userAbsence/UserAbsenceData.kt | 38 + .../models/json/userAbsence/UserAbsenceOCS.kt | 26 + .../json/userAbsence/UserAbsenceOverall.kt | 23 + .../talk/models/json/userprofile/Scope.kt | 15 + .../json/userprofile/UserProfileData.kt | 86 + .../json/userprofile/UserProfileFieldsOCS.kt | 27 + .../userprofile/UserProfileFieldsOverall.kt | 23 + .../models/json/userprofile/UserProfileOCS.kt | 26 + .../json/userprofile/UserProfileOverall.kt | 23 + .../json/websocket/ActorWebSocketMessage.kt | 27 + .../AuthParametersWebSocketMessage.kt | 25 + .../json/websocket/AuthWebSocketMessage.kt | 25 + .../json/websocket/BaseWebSocketMessage.kt | 23 + .../json/websocket/ByeWebSocketMessage.kt | 29 + .../websocket/CallOverallWebSocketMessage.kt | 25 + .../json/websocket/CallWebSocketMessage.kt | 28 + .../websocket/ErrorOverallWebSocketMessage.kt | 25 + .../json/websocket/ErrorWebSocketMessage.kt | 25 + .../websocket/EventOverallWebSocketMessage.kt | 29 + .../websocket/HelloOverallWebSocketMessage.kt | 25 + .../HelloResponseOverallWebSocketMessage.kt | 25 + .../HelloResponseWebSocketMessage.kt | 32 + .../json/websocket/HelloWebSocketMessage.kt | 27 + .../JoinedRoomOverallWebSocketMessage.kt | 25 + .../RoomFederationWebSocketMessage.kt | 28 + .../websocket/RoomOverallWebSocketMessage.kt | 25 + .../RoomPropertiesWebSocketMessage.kt | 27 + .../json/websocket/RoomWebSocketMessage.kt | 29 + ...erHelloResponseFeaturesWebSocketMessage.kt | 23 + .../ListOpenConversationsActivity.kt | 152 + .../adapters/OpenConversationsAdapter.kt | 84 + .../data/OpenConversationsRepository.kt | 15 + .../data/OpenConversationsRepositoryImpl.kt | 32 + .../viewmodels/OpenConversationsViewModel.kt | 77 + .../polls/adapters/PollCreateOptionItem.kt | 9 + .../adapters/PollCreateOptionViewHolder.kt | 79 + .../adapters/PollCreateOptionsAdapter.kt | 45 + .../adapters/PollCreateOptionsItemListener.kt | 18 + .../polls/adapters/PollResultHeaderItem.kt | 18 + .../adapters/PollResultHeaderViewHolder.kt | 40 + .../talk/polls/adapters/PollResultItem.kt | 11 + .../adapters/PollResultItemClickListener.kt | 11 + .../polls/adapters/PollResultViewHolder.kt | 14 + .../polls/adapters/PollResultVoterItem.kt | 19 + .../adapters/PollResultVoterViewHolder.kt | 70 + .../adapters/PollResultVotersOverviewItem.kt | 20 + .../PollResultVotersOverviewViewHolder.kt | 108 + .../talk/polls/adapters/PollResultsAdapter.kt | 78 + .../com/nextcloud/talk/polls/model/Poll.kt | 32 + .../nextcloud/talk/polls/model/PollDetails.kt | 16 + .../talk/polls/repositories/PollRepository.kt | 28 + .../polls/repositories/PollRepositoryImpl.kt | 115 + .../repositories/model/PollDetailsResponse.kt | 33 + .../talk/polls/repositories/model/PollOCS.kt | 22 + .../polls/repositories/model/PollOverall.kt | 22 + .../polls/repositories/model/PollResponse.kt | 60 + .../talk/polls/ui/PollCreateDialogFragment.kt | 197 + .../talk/polls/ui/PollLoadingFragment.kt | 47 + .../talk/polls/ui/PollMainDialogFragment.kt | 188 + .../talk/polls/ui/PollResultsFragment.kt | 154 + .../talk/polls/ui/PollVoteFragment.kt | 215 + .../polls/viewmodels/PollCreateViewModel.kt | 191 + .../polls/viewmodels/PollMainViewModel.kt | 173 + .../polls/viewmodels/PollResultsViewModel.kt | 111 + .../polls/viewmodels/PollVoteViewModel.kt | 118 + .../MentionAutocompletePresenter.java | 194 + .../nextcloud/talk/profile/ProfileActivity.kt | 780 + .../talk/raisehand/RequestAssistanceModel.kt | 9 + .../raisehand/RequestAssistanceRepository.kt | 16 + .../RequestAssistanceRepositoryImpl.kt | 61 + .../WithdrawRequestAssistanceModel.kt | 9 + .../raisehand/viewmodel/RaiseHandViewModel.kt | 121 + .../talk/receivers/DirectReplyReceiver.kt | 186 + .../DismissRecordingAvailableReceiver.kt | 99 + .../talk/receivers/MarkAsReadReceiver.kt | 109 + .../talk/receivers/PackageReplacedReceiver.kt | 24 + .../receivers/ShareRecordingToChatReceiver.kt | 110 + .../remotefilebrowser/SelectionInterface.kt | 12 + .../activities/RemoteFileBrowserActivity.kt | 279 + .../adapters/RemoteFileBrowserItemsAdapter.kt | 76 + .../RemoteFileBrowserItemsListViewHolder.kt | 127 + .../RemoteFileBrowserItemsViewHolder.kt | 29 + .../model/RemoteFileBrowserItem.kt | 29 + .../RemoteFileBrowserItemsRepository.kt | 15 + .../RemoteFileBrowserItemsRepositoryImpl.kt | 41 + .../RemoteFileBrowserItemsViewModel.kt | 221 + .../callrecording/CallRecordingRepository.kt | 18 + .../CallRecordingRepositoryImpl.kt | 64 + .../conversations/ConversationsRepository.kt | 58 + .../ConversationsRepositoryImpl.kt | 176 + .../reactions/ReactionsRepository.kt | 19 + .../reactions/ReactionsRepositoryImpl.kt | 163 + .../unifiedsearch/UnifiedSearchRepository.kt | 31 + .../UnifiedSearchRepositoryImpl.kt | 94 + .../talk/settings/SettingsActivity.kt | 1441 + .../activities/SharedItemsActivity.kt | 236 + .../adapters/SharedItemsAdapter.kt | 89 + .../adapters/SharedItemsGridViewHolder.kt | 29 + .../adapters/SharedItemsListViewHolder.kt | 130 + .../adapters/SharedItemsViewHolder.kt | 92 + .../shareditems/model/SharedDeckCardItem.kt | 18 + .../talk/shareditems/model/SharedFileItem.kt | 22 + .../talk/shareditems/model/SharedItem.kt | 15 + .../talk/shareditems/model/SharedItemType.kt | 28 + .../talk/shareditems/model/SharedItems.kt | 15 + .../shareditems/model/SharedLocationItem.kt | 18 + .../talk/shareditems/model/SharedOtherItem.kt | 15 + .../talk/shareditems/model/SharedPollItem.kt | 15 + .../repositories/SharedItemsRepository.kt | 23 + .../repositories/SharedItemsRepositoryImpl.kt | 206 + .../viewmodels/SharedItemsViewModel.kt | 163 + .../CallParticipantMessageNotifier.java | 94 + .../signaling/ConversationMessageNotifier.kt | 37 + .../LocalParticipantMessageNotifier.java | 40 + .../talk/signaling/OfferMessageNotifier.java | 40 + .../ParticipantListMessageNotifier.java | 55 + .../signaling/SignalingMessageReceiver.java | 844 + .../signaling/SignalingMessageSender.java | 23 + .../talk/signaling/WebRtcMessageNotifier.java | 107 + .../ThreadsOverviewActivity.kt | 257 + .../threadsoverview/components/ThreadRow.kt | 213 + .../threadsoverview/data/ThreadsRepository.kt | 20 + .../data/ThreadsRepositoryImpl.kt | 36 + .../viewmodels/ThreadsOverviewViewModel.kt | 80 + .../repositories/TranslateRepository.kt | 23 + .../repositories/TranslateRepositoryImpl.kt | 31 + .../translate/repositories/model/Language.kt | 28 + .../repositories/model/LanguagesData.kt | 24 + .../repositories/model/LanguagesOCS.kt | 25 + .../repositories/model/LanguagesOverall.kt | 22 + .../repositories/model/TranslateData.kt | 24 + .../repositories/model/TranslateOCS.kt | 25 + .../repositories/model/TranslationsOverall.kt | 22 + .../talk/translate/ui/TranslateActivity.kt | 290 + .../viewmodels/TranslateViewModel.kt | 119 + .../talk/ui/BackgroundVoiceMessageCard.kt | 195 + .../nextcloud/talk/ui/ComposeChatAdapter.kt | 1004 + .../com/nextcloud/talk/ui/MessageInput.kt | 81 + .../com/nextcloud/talk/ui/MicInputCloud.kt | 382 + .../nextcloud/talk/ui/PlaybackSpeedControl.kt | 71 + .../com/nextcloud/talk/ui/StatusDrawable.java | 144 + .../com/nextcloud/talk/ui/WaveformSeekBar.kt | 125 + .../ui/bottom/sheet/ProfileBottomSheet.kt | 184 + .../talk/ui/dialog/AttachmentDialog.kt | 151 + .../talk/ui/dialog/AudioOutputDialog.kt | 146 + .../dialog/ChooseAccountDialogFragment.java | 416 + .../ChooseAccountShareToDialogFragment.kt | 161 + .../talk/ui/dialog/ContextChatCompose.kt | 262 + .../dialog/ConversationsListBottomDialog.kt | 457 + .../talk/ui/dialog/DateTimeCompose.kt | 421 + .../talk/ui/dialog/DialogBanListFragment.kt | 140 + .../dialog/FileAttachmentPreviewFragment.kt | 95 + .../ui/dialog/FilterConversationFragment.kt | 172 + .../talk/ui/dialog/MessageActionsDialog.kt | 638 + .../talk/ui/dialog/MoreCallActionsDialog.kt | 190 + .../OnlineStatusBottomDialogFragment.kt | 184 + .../ui/dialog/SaveToStorageDialogFragment.kt | 115 + .../nextcloud/talk/ui/dialog/ScopeDialog.kt | 80 + .../ui/dialog/SetPhoneNumberDialogFragment.kt | 101 + .../talk/ui/dialog/ShowReactionsDialog.kt | 357 + .../ui/dialog/SortingOrderDialogFragment.java | 189 + .../StatusMessageBottomDialogFragment.kt | 600 + .../ui/dialog/TempMessageActionsDialog.kt | 130 + .../ui/recyclerview/MessageSwipeActions.kt | 22 + .../ui/recyclerview/MessageSwipeCallback.kt | 276 + .../talk/ui/theme/MaterialSchemesProvider.kt | 18 + .../ui/theme/MaterialSchemesProviderImpl.kt | 50 + .../talk/ui/theme/ServerThemeImpl.kt | 30 + .../ui/theme/TalkSpecificViewThemeUtils.kt | 454 + .../nextcloud/talk/ui/theme/ThemeModule.kt | 30 + .../nextcloud/talk/ui/theme/ViewThemeUtils.kt | 31 + .../nextcloud/talk/upload/chunked/Chunk.kt | 12 + .../chunked/ChunkFromFileRequestBody.kt | 102 + .../upload/chunked/ChunkedFileUploader.kt | 396 + .../chunked/OnDataTransferProgressListener.kt | 12 + .../talk/upload/normal/FileUploader.kt | 180 + .../com/nextcloud/talk/users/UserManager.kt | 236 + .../com/nextcloud/talk/utils/AccountUtils.kt | 148 + .../java/com/nextcloud/talk/utils/ApiUtils.kt | 542 + .../talk/utils/AppCompatActivityExtensions.kt | 38 + .../com/nextcloud/talk/utils/AudioUtils.kt | 231 + .../talk/utils/AuthenticatorService.java | 87 + .../nextcloud/talk/utils/BitmapShrinker.kt | 86 + .../com/nextcloud/talk/utils/BrandingUtils.kt | 16 + .../nextcloud/talk/utils/CapabilitiesUtil.kt | 334 + .../com/nextcloud/talk/utils/CharPolicy.java | 110 + .../nextcloud/talk/utils/ChatMessageUtils.kt | 38 + .../nextcloud/talk/utils/ColorGenerator.kt | 69 + .../com/nextcloud/talk/utils/ContactUtils.kt | 44 + .../nextcloud/talk/utils/ContextExtensions.kt | 39 + .../nextcloud/talk/utils/ConversationUtils.kt | 61 + .../com/nextcloud/talk/utils/DateConstants.kt | 16 + .../com/nextcloud/talk/utils/DateUtils.kt | 106 + .../com/nextcloud/talk/utils/DeviceUtils.java | 79 + .../com/nextcloud/talk/utils/DisplayUtils.kt | 538 + .../nextcloud/talk/utils/DoNotDisturbUtils.kt | 47 + .../com/nextcloud/talk/utils/DrawableUtils.kt | 183 + .../talk/utils/EmojiTextInputEditText.java | 59 + .../utils/FABAwareScrollingViewBehavior.java | 62 + .../com/nextcloud/talk/utils/FileSortOrder.kt | 78 + .../talk/utils/FileSortOrderByDate.kt | 32 + .../talk/utils/FileSortOrderByName.kt | 44 + .../talk/utils/FileSortOrderBySize.kt | 41 + .../com/nextcloud/talk/utils/FileUtils.kt | 166 + .../nextcloud/talk/utils/FileViewerUtils.kt | 411 + .../talk/utils/ImageEmojiEditText.kt | 62 + .../com/nextcloud/talk/utils/LoggingUtils.kt | 50 + .../java/com/nextcloud/talk/utils/Mimetype.kt | 38 + .../com/nextcloud/talk/utils/MimetypeUtils.kt | 15 + .../talk/utils/NoSupportedApiException.kt | 9 + .../nextcloud/talk/utils/NotificationUtils.kt | 346 + .../talk/utils/ParticipantPermissions.kt | 86 + .../com/nextcloud/talk/utils/PickImage.kt | 179 + .../com/nextcloud/talk/utils/PushUtils.kt | 412 + .../com/nextcloud/talk/utils/ReceiverFlag.kt | 29 + .../nextcloud/talk/utils/RemoteFileUtils.kt | 72 + .../nextcloud/talk/utils/SecurityUtils.java | 116 + .../com/nextcloud/talk/utils/ShareUtils.kt | 58 + .../com/nextcloud/talk/utils/SyncAdapter.java | 29 + .../com/nextcloud/talk/utils/SyncService.java | 36 + .../com/nextcloud/talk/utils/TextDrawable.kt | 59 + .../nextcloud/talk/utils/TextMatchers.java | 23 + .../java/com/nextcloud/talk/utils/UriUtils.kt | 65 + .../com/nextcloud/talk/utils/UserIdUtils.kt | 20 + .../nextcloud/talk/utils/VibrationUtils.kt | 20 + .../talk/utils/animations/PulseAnimation.java | 72 + .../ViewHidingBehaviourAnimation.java | 61 + .../nextcloud/talk/utils/bundle/BundleKeys.kt | 87 + .../ArbitraryStorageModule.java | 33 + .../database/user/CurrentUserProviderImpl.kt | 44 + .../database/user/CurrentUserProviderNew.kt | 15 + .../talk/utils/database/user/UserModule.kt | 28 + .../talk/utils/message/MessageUtils.kt | 228 + .../talk/utils/message/SendMessageUtils.kt | 32 + .../permissions/PlatformPermissionUtil.kt | 18 + .../permissions/PlatformPermissionUtilImpl.kt | 103 + .../talk/utils/power/PowerManagerUtils.kt | 176 + .../talk/utils/power/ProximityLock.java | 45 + .../utils/preferences/AppPreferences.java | 191 + .../utils/preferences/AppPreferencesImpl.kt | 649 + .../DatabaseStorageModule.kt | 200 + .../talk/utils/preview/ComposePreviewUtils.kt | 194 + .../utils/preview/ComposePreviewUtilsDaos.kt | 229 + .../nextcloud/talk/utils/rx/DisposableSet.kt | 23 + .../talk/utils/rx/SearchViewObservable.kt | 34 + .../ApplicationWideCurrentRoomHolder.java | 95 + .../ApplicationWideMessageHolder.java | 33 + .../singletons/AvatarStatusCodeHolder.java | 24 + .../nextcloud/talk/utils/ssl/KeyManager.java | 191 + .../talk/utils/ssl/SSLSocketFactoryCompat.kt | 101 + .../talk/utils/ssl/TrustManager.java | 183 + .../com/nextcloud/talk/utils/text/Spans.java | 84 + .../talk/viewmodels/CallRecordingViewModel.kt | 159 + .../talk/viewmodels/GeoCodingViewModel.kt | 64 + .../webrtc/DataChannelMessageNotifier.java | 65 + .../com/nextcloud/talk/webrtc/Globals.java | 15 + .../talk/webrtc/PeerConnectionNotifier.java | 55 + .../talk/webrtc/PeerConnectionWrapper.java | 674 + .../talk/webrtc/ProximitySensor.java | 155 + .../nextcloud/talk/webrtc/WebRTCUtils.java | 134 + .../talk/webrtc/WebRtcAudioManager.java | 581 + .../talk/webrtc/WebRtcBluetoothManager.java | 582 + .../webrtc/WebSocketConnectionHelper.java | 171 + .../talk/webrtc/WebSocketInstance.kt | 497 + .../client/TalkJsonNominatimClient.java | 296 + .../daveKoeller/AlphanumComparator.java | 170 + .../third/parties/fresco/BetterImageSpan.kt | 110 + app/src/main/res/anim/popup_animation.xml | 21 + .../res/animator/appbar_elevation_off.xml | 17 + .../main/res/animator/appbar_elevation_on.xml | 17 + .../drawable-land/link_text_background.xml | 19 + .../drawable-night/account_circle_48dp.xml | 17 + .../drawable-night/account_circle_96dp.xml | 16 + .../main/res/drawable-night/ic_cellphone.xml | 16 + .../res/drawable-night/ic_chevron_right.xml | 15 + .../drawable-night/ic_circular_document.xml | 24 + .../res/drawable-night/ic_circular_group.xml | 21 + .../ic_circular_group_mentions.xml | 28 + .../res/drawable-night/ic_circular_link.xml | 21 + .../drawable-night/ic_circular_location.xml | 21 + .../res/drawable-night/ic_circular_lock.xml | 22 + .../res/drawable-night/ic_circular_mail.xml | 21 + .../res/drawable-night/ic_close_search.xml | 15 + .../main/res/drawable-night/ic_contacts.xml | 16 + app/src/main/res/drawable-night/ic_link.xml | 16 + .../main/res/drawable-night/ic_password.xml | 16 + .../drawable-night/icon_circular_phone.xml | 27 + .../res/drawable-night/icon_circular_team.xml | 27 + app/src/main/res/drawable/accent_circle.xml | 11 + .../main/res/drawable/account_circle_48dp.xml | 17 + .../main/res/drawable/account_circle_96dp.xml | 16 + .../main/res/drawable/baseline_article_24.xml | 18 + .../res/drawable/baseline_audiotrack_24.xml | 17 + .../res/drawable/baseline_bar_chart_24.xml | 20 + .../main/res/drawable/baseline_block_24.xml | 17 + .../drawable/baseline_calendar_month_24.xml | 16 + .../drawable/baseline_calendar_today_24.xml | 19 + .../baseline_chat_bubble_outline_24.xml | 16 + .../res/drawable/baseline_contacts_24.xml | 17 + .../res/drawable/baseline_download_24.xml | 16 + .../main/res/drawable/baseline_error_24.xml | 16 + .../drawable/baseline_error_outline_24.xml | 16 + .../baseline_format_list_bulleted_24.xml | 17 + .../main/res/drawable/baseline_image_24.xml | 16 + .../main/res/drawable/baseline_info_24.xml | 16 + .../baseline_insert_drive_file_24.xml | 17 + .../res/drawable/baseline_location_pin_24.xml | 16 + .../res/drawable/baseline_lock_open_24.xml | 16 + app/src/main/res/drawable/baseline_mic_24.xml | 16 + .../drawable/baseline_notifications_24.xml | 16 + .../drawable/baseline_photo_library_24.xml | 16 + .../drawable/baseline_report_problem_24.xml | 16 + .../res/drawable/baseline_schedule_24.xml | 16 + .../main/res/drawable/baseline_stop_24.xml | 16 + .../res/drawable/baseline_tag_faces_24.xml | 18 + .../res/drawable/baseline_unfold_less_24.xml | 15 + .../res/drawable/baseline_unfold_more_24.xml | 15 + .../main/res/drawable/baseline_video_24.xml | 17 + app/src/main/res/drawable/borderless_btn.xml | 14 + .../res/drawable/current_location_circle.xml | 16 + app/src/main/res/drawable/cutout_circle.xml | 11 + app/src/main/res/drawable/deck.xml | 25 + app/src/main/res/drawable/forward_24.xml | 16 + app/src/main/res/drawable/ic_account_plus.xml | 16 + .../main/res/drawable/ic_add_grey600_24px.xml | 16 + .../main/res/drawable/ic_alphabetical_asc.xml | 16 + .../res/drawable/ic_alphabetical_desc.xml | 19 + .../res/drawable/ic_arrow_back_black_24dp.xml | 16 + .../drawable/ic_arrow_forward_white_24px.xml | 16 + .../res/drawable/ic_av_timer_timer_24dp.xml | 17 + .../res/drawable/ic_avatar_background.xml | 28 + .../main/res/drawable/ic_avatar_document.xml | 18 + .../res/drawable/ic_avatar_federation.xml | 15 + app/src/main/res/drawable/ic_avatar_group.xml | 16 + .../res/drawable/ic_avatar_group_small.xml | 22 + app/src/main/res/drawable/ic_avatar_link.xml | 15 + app/src/main/res/drawable/ic_avatar_mail.xml | 15 + .../res/drawable/ic_avatar_team_small.xml | 23 + .../ic_baseline_arrow_downward_24px.xml | 16 + .../drawable/ic_baseline_attach_file_24.xml | 16 + .../res/drawable/ic_baseline_bar_chart_24.xml | 16 + .../ic_baseline_bluetooth_audio_24.xml | 16 + .../res/drawable/ic_baseline_close_24.xml | 16 + .../main/res/drawable/ic_baseline_deck_24.xml | 27 + .../drawable/ic_baseline_do_not_touch_24.xml | 15 + .../ic_baseline_error_outline_24dp.xml | 16 + .../drawable/ic_baseline_filter_list_24.xml | 16 + .../res/drawable/ic_baseline_flash_off_24.xml | 16 + .../res/drawable/ic_baseline_flash_on_24.xml | 16 + .../ic_baseline_flip_camera_android_24.xml | 16 + .../res/drawable/ic_baseline_gps_fixed_24.xml | 15 + .../drawable/ic_baseline_headset_mic_24.xml | 16 + .../res/drawable/ic_baseline_keyboard_24.xml | 16 + .../drawable/ic_baseline_location_on_24.xml | 16 + .../ic_baseline_location_on_red_24.xml | 15 + .../main/res/drawable/ic_baseline_mic_24.xml | 16 + .../res/drawable/ic_baseline_mic_red_24.xml | 16 + .../ic_baseline_notifications_off_24.xml | 16 + .../ic_baseline_pause_voice_message_24.xml | 15 + .../res/drawable/ic_baseline_person_24.xml | 16 + .../drawable/ic_baseline_phone_in_talk_24.xml | 16 + .../drawable/ic_baseline_phone_missed_24.xml | 17 + .../drawable/ic_baseline_photo_camera_24.xml | 16 + .../ic_baseline_picture_in_picture_alt_24.xml | 17 + ...c_baseline_play_arrow_voice_message_24.xml | 15 + .../res/drawable/ic_baseline_translate_24.xml | 16 + .../res/drawable/ic_baseline_videocam_24.xml | 17 + .../main/res/drawable/ic_call_black_24dp.xml | 15 + .../res/drawable/ic_call_end_white_24px.xml | 15 + .../res/drawable/ic_call_grey_600_24dp.xml | 16 + .../main/res/drawable/ic_call_white_24dp.xml | 15 + .../res/drawable/ic_cancel_black_24dp.xml | 15 + .../res/drawable/ic_cancel_white_24dp.xml | 16 + app/src/main/res/drawable/ic_cellphone.xml | 15 + app/src/main/res/drawable/ic_check.xml | 16 + app/src/main/res/drawable/ic_check_24.xml | 16 + app/src/main/res/drawable/ic_check_all.xml | 16 + .../main/res/drawable/ic_check_black_24dp.xml | 16 + app/src/main/res/drawable/ic_check_circle.xml | 16 + .../res/drawable/ic_check_circle_outlined.xml | 18 + .../main/res/drawable/ic_chevron_right.xml | 15 + .../res/drawable/ic_circular_document.xml | 24 + .../main/res/drawable/ic_circular_group.xml | 21 + .../drawable/ic_circular_group_mentions.xml | 28 + .../main/res/drawable/ic_circular_link.xml | 21 + .../res/drawable/ic_circular_location.xml | 21 + .../main/res/drawable/ic_circular_lock.xml | 22 + .../main/res/drawable/ic_circular_mail.xml | 21 + app/src/main/res/drawable/ic_clear_24.xml | 16 + app/src/main/res/drawable/ic_close_search.xml | 15 + app/src/main/res/drawable/ic_comment.xml | 17 + app/src/main/res/drawable/ic_contacts.xml | 16 + app/src/main/res/drawable/ic_content_copy.xml | 17 + app/src/main/res/drawable/ic_crop_16_9.xml | 16 + app/src/main/res/drawable/ic_crop_4_3.xml | 16 + app/src/main/res/drawable/ic_delete.xml | 16 + .../res/drawable/ic_delete_black_24dp.xml | 15 + .../res/drawable/ic_delete_grey600_24dp.xml | 15 + .../main/res/drawable/ic_dots_horizontal.xml | 16 + .../res/drawable/ic_dots_horizontal_white.xml | 15 + app/src/main/res/drawable/ic_edit.xml | 16 + app/src/main/res/drawable/ic_edit_24.xml | 16 + app/src/main/res/drawable/ic_edit_note_24.xml | 16 + app/src/main/res/drawable/ic_email.xml | 16 + .../drawable/ic_exit_to_app_black_24dp.xml | 11 + app/src/main/res/drawable/ic_eye.xml | 16 + app/src/main/res/drawable/ic_eye_off.xml | 16 + app/src/main/res/drawable/ic_folder.xml | 16 + .../res/drawable/ic_folder_multiple_image.xml | 16 + .../main/res/drawable/ic_hand_back_left.xml | 15 + app/src/main/res/drawable/ic_high_quality.xml | 16 + .../main/res/drawable/ic_info_white_24dp.xml | 16 + .../ic_insert_emoticon_black_24dp.xml | 16 + .../res/drawable/ic_keyboard_arrow_down.xml | 15 + .../res/drawable/ic_keyboard_arrow_up.xml | 21 + .../ic_keyboard_double_arrow_down.xml | 16 + .../res/drawable/ic_launcher_background.xml | 28 + .../res/drawable/ic_launcher_foreground.xml | 24 + app/src/main/res/drawable/ic_link.xml | 16 + .../main/res/drawable/ic_link_black_24px.xml | 15 + .../res/drawable/ic_link_grey600_24px.xml | 15 + .../main/res/drawable/ic_list_empty_error.xml | 16 + .../res/drawable/ic_lock_grey600_24px.xml | 16 + .../drawable/ic_lock_open_grey600_24dp.xml | 16 + .../drawable/ic_lock_plus_grey600_24dp.xml | 16 + .../main/res/drawable/ic_lock_white_24px.xml | 15 + app/src/main/res/drawable/ic_logo.xml | 23 + app/src/main/res/drawable/ic_low_quality.xml | 16 + app/src/main/res/drawable/ic_map_marker.xml | 15 + app/src/main/res/drawable/ic_menu.xml | 15 + .../res/drawable/ic_mic_grey_600_24dp.xml | 16 + .../res/drawable/ic_mic_off_white_24px.xml | 15 + .../main/res/drawable/ic_mic_white_24px.xml | 15 + .../drawable/ic_mimetype_application_pdf.xml | 16 + .../main/res/drawable/ic_mimetype_audio.xml | 16 + .../main/res/drawable/ic_mimetype_file.xml | 18 + .../main/res/drawable/ic_mimetype_folder.xml | 16 + .../drawable/ic_mimetype_folder_shared.xml | 15 + .../main/res/drawable/ic_mimetype_image.xml | 16 + .../main/res/drawable/ic_mimetype_link.xml | 38 + .../res/drawable/ic_mimetype_location.xml | 17 + .../ic_mimetype_package_x_generic.xml | 18 + .../main/res/drawable/ic_mimetype_text.xml | 17 + .../drawable/ic_mimetype_text_calendar.xml | 17 + .../res/drawable/ic_mimetype_text_code.xml | 22 + .../res/drawable/ic_mimetype_text_vcard.xml | 17 + .../main/res/drawable/ic_mimetype_video.xml | 16 + .../ic_mimetype_x_office_document.xml | 17 + .../ic_mimetype_x_office_presentation.xml | 15 + .../ic_mimetype_x_office_spreadsheet.xml | 16 + .../main/res/drawable/ic_modification_asc.xml | 14 + .../res/drawable/ic_modification_desc.xml | 14 + .../res/drawable/ic_more_horiz_black_24dp.xml | 16 + app/src/main/res/drawable/ic_note_to_self.xml | 26 + app/src/main/res/drawable/ic_notification.xml | 23 + app/src/main/res/drawable/ic_password.xml | 16 + .../res/drawable/ic_pencil_grey600_24dp.xml | 16 + .../drawable/ic_people_group_black_24px.xml | 16 + app/src/main/res/drawable/ic_phone.xml | 18 + app/src/main/res/drawable/ic_phone_small.xml | 24 + app/src/main/res/drawable/ic_refresh.xml | 15 + app/src/main/res/drawable/ic_reply.xml | 17 + .../drawable/ic_room_service_black_24dp.xml | 15 + app/src/main/res/drawable/ic_search_grey.xml | 15 + .../res/drawable/ic_search_white_24dp.xml | 16 + .../res/drawable/ic_security_white_24dp.xml | 15 + app/src/main/res/drawable/ic_settings.xml | 16 + app/src/main/res/drawable/ic_share_action.xml | 17 + .../main/res/drawable/ic_share_variant.xml | 16 + .../ic_signal_wifi_off_white_24dp.xml | 15 + app/src/main/res/drawable/ic_size_asc.xml | 16 + app/src/main/res/drawable/ic_size_desc.xml | 16 + .../main/res/drawable/ic_star_black_24dp.xml | 16 + .../drawable/ic_star_border_black_24dp.xml | 16 + .../drawable/ic_switch_video_white_24px.xml | 15 + app/src/main/res/drawable/ic_talk.xml | 18 + .../main/res/drawable/ic_timer_black_24dp.xml | 16 + app/src/main/res/drawable/ic_twitter.xml | 16 + app/src/main/res/drawable/ic_user.xml | 16 + .../main/res/drawable/ic_user_status_away.xml | 15 + .../main/res/drawable/ic_user_status_busy.xml | 19 + .../main/res/drawable/ic_user_status_dnd.xml | 15 + .../res/drawable/ic_user_status_invisible.xml | 15 + .../drawable/ic_videocam_grey_600_24dp.xml | 17 + .../drawable/ic_videocam_off_white_24px.xml | 15 + .../res/drawable/ic_videocam_white_24px.xml | 16 + .../drawable/ic_volume_mute_white_24dp.xml | 16 + .../res/drawable/ic_volume_up_white_24dp.xml | 16 + .../main/res/drawable/ic_warning_white.xml | 15 + app/src/main/res/drawable/ic_web.xml | 15 + .../main/res/drawable/icon_circular_phone.xml | 27 + .../main/res/drawable/icon_circular_team.xml | 27 + .../main/res/drawable/incoming_gradient.xml | 14 + app/src/main/res/drawable/launch_screen.xml | 15 + .../res/drawable/link_text_background.xml | 19 + .../link_text_no_preview_background.xml | 19 + app/src/main/res/drawable/mention_chip.xml | 11 + app/src/main/res/drawable/online_status.xml | 16 + .../main/res/drawable/outline_archive_24.xml | 18 + .../main/res/drawable/outline_forum_24.xml | 18 + .../outline_notifications_active_24.xml | 11 + .../main/res/drawable/outline_qr_code_24.xml | 12 + .../res/drawable/reaction_self_background.xml | 23 + .../reaction_self_bottom_sheet_background.xml | 17 + app/src/main/res/drawable/record_start.xml | 15 + app/src/main/res/drawable/record_starting.xml | 15 + app/src/main/res/drawable/record_stop.xml | 15 + .../main/res/drawable/reply_background.xml | 33 + app/src/main/res/drawable/round_bgnd.xml | 11 + .../shape_grouped_incoming_message.xml | 19 + .../shape_grouped_outcoming_message.xml | 19 + .../res/drawable/shape_incoming_message.xml | 19 + .../res/drawable/shape_outcoming_message.xml | 19 + app/src/main/res/drawable/shape_oval.xml | 11 + app/src/main/res/drawable/trashbin.xml | 16 + app/src/main/res/drawable/upload.xml | 15 + app/src/main/res/drawable/upload_white.xml | 15 + ...voice_message_outgoing_seek_bar_slider.xml | 15 + .../main/res/layout-land/activity_profile.xml | 194 + .../res/layout-land/activity_translate.xml | 158 + .../layout-land/reference_inside_message.xml | 90 + app/src/main/res/layout/account_item.xml | 103 + .../layout/activity_account_verification.xml | 46 + app/src/main/res/layout/activity_chat.xml | 291 + .../res/layout/activity_conversation_info.xml | 659 + .../activity_conversation_info_edit.xml | 159 + .../res/layout/activity_conversations.xml | 341 + .../res/layout/activity_full_screen_image.xml | 38 + .../res/layout/activity_full_screen_media.xml | 31 + .../res/layout/activity_full_screen_text.xml | 53 + .../main/res/layout/activity_geocoding.xml | 38 + .../main/res/layout/activity_invitations.xml | 55 + app/src/main/res/layout/activity_location.xml | 136 + app/src/main/res/layout/activity_locked.xml | 51 + app/src/main/res/layout/activity_main.xml | 45 + .../res/layout/activity_message_search.xml | 59 + .../layout/activity_open_conversations.xml | 90 + app/src/main/res/layout/activity_profile.xml | 169 + .../layout/activity_remote_file_browser.xml | 118 + .../res/layout/activity_server_selection.xml | 195 + app/src/main/res/layout/activity_settings.xml | 914 + .../main/res/layout/activity_shared_items.xml | 61 + .../res/layout/activity_switch_account.xml | 43 + .../main/res/layout/activity_take_picture.xml | 223 + .../main/res/layout/activity_translate.xml | 151 + .../res/layout/activity_web_view_login.xml | 35 + app/src/main/res/layout/ban_item_list.xml | 72 + app/src/main/res/layout/call_activity.xml | 365 + .../res/layout/call_notification_activity.xml | 109 + .../main/res/layout/call_started_message.xml | 115 + app/src/main/res/layout/call_states.xml | 55 + .../main/res/layout/create_thread_view.xml | 52 + .../main/res/layout/current_account_item.xml | 130 + app/src/main/res/layout/dialog_attachment.xml | 338 + .../main/res/layout/dialog_audio_output.xml | 164 + .../res/layout/dialog_ban_participant.xml | 107 + .../main/res/layout/dialog_choose_account.xml | 144 + .../layout/dialog_choose_account_share_to.xml | 38 + .../layout/dialog_conversation_operations.xml | 323 + .../res/layout/dialog_create_conversation.xml | 55 + .../layout/dialog_file_attachment_preview.xml | 95 + .../res/layout/dialog_filter_conversation.xml | 84 + .../res/layout/dialog_message_actions.xml | 652 + .../res/layout/dialog_message_reactions.xml | 39 + .../res/layout/dialog_more_call_actions.xml | 119 + app/src/main/res/layout/dialog_password.xml | 27 + .../main/res/layout/dialog_poll_create.xml | 155 + .../main/res/layout/dialog_poll_loading.xml | 22 + app/src/main/res/layout/dialog_poll_main.xml | 83 + .../main/res/layout/dialog_poll_results.xml | 57 + app/src/main/res/layout/dialog_poll_vote.xml | 82 + .../res/layout/dialog_rename_conversation.xml | 56 + app/src/main/res/layout/dialog_scope.xml | 206 + .../res/layout/dialog_set_online_status.xml | 379 + .../res/layout/dialog_set_phone_number.xml | 29 + .../res/layout/dialog_set_status_message.xml | 157 + .../layout/dialog_temp_message_actions.xml | 165 + app/src/main/res/layout/edit_message_view.xml | 65 + app/src/main/res/layout/empty_list.xml | 54 + .../res/layout/federated_invitation_hint.xml | 39 + .../res/layout/fragment_dialog_ban_list.xml | 56 + .../res/layout/fragment_message_input.xml | 77 + ...fragment_message_input_voice_recording.xml | 113 + app/src/main/res/layout/geocoding_item.xml | 43 + ...item_custom_incoming_deck_card_message.xml | 119 + ...m_custom_incoming_link_preview_message.xml | 97 + .../item_custom_incoming_location_message.xml | 83 + .../item_custom_incoming_poll_message.xml | 104 + .../item_custom_incoming_preview_message.xml | 212 + .../item_custom_incoming_text_message.xml | 117 + ...m_custom_incoming_text_message_shimmer.xml | 70 + .../item_custom_incoming_voice_message.xml | 145 + ...tem_custom_outcoming_deck_card_message.xml | 113 + ..._custom_outcoming_link_preview_message.xml | 86 + ...item_custom_outcoming_location_message.xml | 78 + .../item_custom_outcoming_poll_message.xml | 97 + .../item_custom_outcoming_preview_message.xml | 185 + .../item_custom_outcoming_text_message.xml | 132 + .../item_custom_outcoming_voice_message.xml | 131 + .../main/res/layout/item_event_schedule.xml | 60 + .../res/layout/item_guest_access_settings.xml | 138 + .../main/res/layout/item_message_quote.xml | 96 + .../res/layout/item_notification_settings.xml | 158 + .../main/res/layout/item_reactions_tab.xml | 33 + .../res/layout/item_recording_consent.xml | 77 + app/src/main/res/layout/item_spacer.xml | 10 + .../main/res/layout/item_system_message.xml | 98 + app/src/main/res/layout/item_thread_title.xml | 42 + app/src/main/res/layout/item_webinar_info.xml | 119 + app/src/main/res/layout/lobby_view.xml | 38 + app/src/main/res/layout/menu_item_sheet.xml | 41 + .../res/layout/no_saved_messages_view.xml | 37 + .../main/res/layout/notifications_warning.xml | 66 + .../main/res/layout/out_of_office_view.xml | 113 + .../res/layout/poll_create_options_item.xml | 56 + .../res/layout/poll_result_header_item.xml | 47 + .../res/layout/poll_result_voter_item.xml | 31 + .../poll_result_voters_overview_item.xml | 16 + app/src/main/res/layout/predefined_status.xml | 75 + app/src/main/res/layout/reaction_item.xml | 43 + .../res/layout/reactions_inside_message.xml | 35 + .../res/layout/reference_inside_message.xml | 87 + .../remainder_to_delete_conversation.xml | 56 + .../main/res/layout/rv_item_browser_file.xml | 91 + app/src/main/res/layout/rv_item_contact.xml | 52 + .../rv_item_conversation_info_participant.xml | 106 + ...rv_item_conversation_with_last_message.xml | 135 + ...conversation_with_last_message_shimmer.xml | 46 + .../main/res/layout/rv_item_invitation.xml | 88 + app/src/main/res/layout/rv_item_load_more.xml | 45 + .../res/layout/rv_item_notification_sound.xml | 34 + .../res/layout/rv_item_open_conversation.xml | 54 + .../res/layout/rv_item_search_message.xml | 64 + .../main/res/layout/rv_item_title_header.xml | 34 + app/src/main/res/layout/search_layout.xml | 96 + app/src/main/res/layout/shared_item_grid.xml | 45 + app/src/main/res/layout/shared_item_list.xml | 131 + .../res/layout/sorting_order_fragment.xml | 299 + .../layout/user_info_details_table_item.xml | 70 + .../user_info_details_table_item_shimmer.xml | 30 + .../main/res/layout/view_message_input.xml | 267 + app/src/main/res/menu/chat_call_menu.xml | 13 + app/src/main/res/menu/chat_send_menu.xml | 13 + app/src/main/res/menu/menu_conversation.xml | 80 + .../main/res/menu/menu_conversation_info.xml | 17 + .../res/menu/menu_conversation_info_edit.xml | 17 + .../menu/menu_conversation_plus_filter.xml | 29 + app/src/main/res/menu/menu_geocoding.xml | 19 + app/src/main/res/menu/menu_locationpicker.xml | 19 + app/src/main/res/menu/menu_preview.xml | 15 + app/src/main/res/menu/menu_profile.xml | 17 + app/src/main/res/menu/menu_search.xml | 16 + app/src/main/res/menu/menu_share_files.xml | 15 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 12 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4803 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 3011 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 6716 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 10398 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 14973 bytes .../res/raw/librem_by_feandesign_call.ogg | Bin 0 -> 213909 bytes .../res/raw/librem_by_feandesign_message.ogg | Bin 0 -> 82206 bytes .../res/raw/next_voice_message_doodle.ogg | Bin 0 -> 10580 bytes .../main/res/raw/tr110_1_kap8_3_freiton1.ogg | Bin 0 -> 34121 bytes app/src/main/res/values-ar/strings.xml | 674 + app/src/main/res/values-ast/strings.xml | 359 + app/src/main/res/values-b+en+001/strings.xml | 726 + app/src/main/res/values-be/strings.xml | 683 + app/src/main/res/values-bg-rBG/strings.xml | 514 + app/src/main/res/values-ca/strings.xml | 650 + app/src/main/res/values-cs-rCZ/strings.xml | 728 + app/src/main/res/values-da/strings.xml | 659 + app/src/main/res/values-de/strings.xml | 727 + app/src/main/res/values-el/strings.xml | 425 + app/src/main/res/values-es-rEC/strings.xml | 526 + app/src/main/res/values-es/strings.xml | 732 + app/src/main/res/values-et-rEE/strings.xml | 727 + app/src/main/res/values-eu/strings.xml | 590 + app/src/main/res/values-fa/strings.xml | 433 + app/src/main/res/values-fi-rFI/strings.xml | 438 + app/src/main/res/values-fr/strings.xml | 727 + app/src/main/res/values-ga/strings.xml | 739 + app/src/main/res/values-gl/strings.xml | 727 + app/src/main/res/values-hr/strings.xml | 408 + app/src/main/res/values-hu-rHU/strings.xml | 701 + app/src/main/res/values-it/strings.xml | 487 + app/src/main/res/values-ja-rJP/strings.xml | 612 + app/src/main/res/values-ko/strings.xml | 508 + app/src/main/res/values-land/dimens.xml | 13 + app/src/main/res/values-lt-rLT/strings.xml | 398 + app/src/main/res/values-nb-rNO/strings.xml | 633 + app/src/main/res/values-night/colors.xml | 67 + app/src/main/res/values-nl/strings.xml | 702 + app/src/main/res/values-pl/strings.xml | 735 + app/src/main/res/values-pt-rBR/strings.xml | 730 + app/src/main/res/values-ru/strings.xml | 722 + app/src/main/res/values-sc/strings.xml | 403 + app/src/main/res/values-sk-rSK/strings.xml | 666 + app/src/main/res/values-sl/strings.xml | 525 + app/src/main/res/values-sr/strings.xml | 731 + app/src/main/res/values-sv/strings.xml | 722 + app/src/main/res/values-sw/strings.xml | 729 + app/src/main/res/values-sw600dp/dimens.xml | 13 + app/src/main/res/values-tr/strings.xml | 727 + app/src/main/res/values-ug/strings.xml | 642 + app/src/main/res/values-uk/strings.xml | 719 + app/src/main/res/values-v27/styles.xml | 33 + app/src/main/res/values-v28/arrays.xml | 64 + app/src/main/res/values-v28/defaults.xml | 10 + app/src/main/res/values-w820dp/dimens.xml | 13 + app/src/main/res/values-zh-rCN/strings.xml | 586 + app/src/main/res/values-zh-rHK/strings.xml | 723 + app/src/main/res/values-zh-rTW/strings.xml | 723 + app/src/main/res/values/arrays.xml | 83 + app/src/main/res/values/attrs.xml | 14 + app/src/main/res/values/bool.xml | 11 + app/src/main/res/values/colors.xml | 99 + app/src/main/res/values/defaults.xml | 10 + app/src/main/res/values/dimens.xml | 92 + app/src/main/res/values/setup.xml | 59 + app/src/main/res/values/strings.xml | 905 + app/src/main/res/values/styles.xml | 310 + app/src/main/res/xml/auth.xml | 12 + app/src/main/res/xml/backup_config.xml | 10 + app/src/main/res/xml/chip_others.xml | 14 + app/src/main/res/xml/chip_you.xml | 14 + app/src/main/res/xml/contacts.xml | 13 + app/src/main/res/xml/file_provider_paths.xml | 14 + .../main/res/xml/network_security_config.xml | 16 + app/src/main/res/xml/syncadapter.xml | 11 + app/src/qa/ic_launcher-web.png | Bin 0 -> 31078 bytes .../talk/utils/ClosedInterfaceImpl.java | 28 + .../res/drawable/ic_launcher_background.xml | 32 + .../res/drawable/ic_launcher_foreground.xml | 21 + .../qa/res/mipmap-anydpi-v26/ic_launcher.xml | 12 + app/src/qa/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 5184 bytes app/src/qa/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 3229 bytes app/src/qa/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 7299 bytes app/src/qa/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 11233 bytes app/src/qa/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 16182 bytes app/src/qa/res/values/setup.xml | 12 + app/src/test/java/android/util/Log.kt | 56 + ...lParticipantListExternalSignalingTest.java | 654 + ...lParticipantListInternalSignalingTest.java | 528 + .../talk/call/CallParticipantListTest.java | 48 + .../talk/call/CallParticipantModelTest.kt | 52 + .../call/LocalCallParticipantModelTest.kt | 168 + .../talk/call/LocalStateBroadcasterMcuTest.kt | 641 + .../call/LocalStateBroadcasterNoMcuTest.kt | 357 + .../talk/call/LocalStateBroadcasterTest.kt | 324 + .../talk/call/MessageSenderMcuTest.kt | 118 + .../talk/call/MessageSenderNoMcuTest.kt | 101 + .../nextcloud/talk/call/MessageSenderTest.kt | 134 + .../talk/contacts/ContactsViewModelTest.kt | 126 + .../talk/contacts/apiService/FakeItem.kt | 58 + .../repository/FakeRepositoryError.kt | 29 + .../repository/FakeRepositorySuccess.kt | 21 + .../ConversationInfoViewModelTest.kt | 25 + .../talk/json/ConversationConversionTest.kt | 174 + .../talk/login/data/LoginRepositoryTest.kt | 596 + .../network/NetworkLoginDataSourceTest.kt | 167 + .../messagesearch/MessageSearchHelperTest.kt | 127 + ...ingMessageReceiverCallParticipantTest.java | 262 + ...ngMessageReceiverLocalParticipantTest.java | 180 + .../SignalingMessageReceiverOfferTest.java | 218 + ...ingMessageReceiverParticipantListTest.java | 461 + .../SignalingMessageReceiverTest.java | 122 + .../SignalingMessageReceiverWebRtcTest.java | 353 + .../test/fakes/FakeCallRecordingRepository.kt | 20 + .../test/fakes/FakeUnifiedSearchRepository.kt | 37 + .../nextcloud/talk/utils/BundleKeysTest.kt | 90 + .../talk/utils/DoNotDisturbUtilsTest.java | 60 + .../talk/utils/ParticipantPermissionsTest.kt | 97 + .../nextcloud/talk/utils/UserIdUtilsTest.kt | 42 + .../talk/viewmodels/AbstractViewModelTest.kt | 40 + .../viewmodels/CallRecordingViewModelTest.kt | 114 + .../webrtc/DataChannelMessageNotifierTest.kt | 75 + .../com/nextcloud/talk/webrtc/GlobalsTest.kt | 27 + .../talk/webrtc/PeerConnectionNotifierTest.kt | 68 + .../talk/webrtc/PeerConnectionWrapperTest.kt | 760 + .../resources/RoomOverallExample_APIv1.json | 66 + .../resources/RoomOverallExample_APIv2.json | 51 + .../resources/RoomOverallExample_APIv4.json | 83 + .../org.mockito.plugins.MockMaker | 1 + build.gradle | 51 + contribute/developer-certificate-of-origin | 35 + .../developer-certificate-of-origin.license | 2 + detekt.yml | 493 + docs/TURN.md | 96 + docs/branching.png | Bin 0 -> 22626 bytes docs/branching.png.license | 2 + docs/branching.svg | 187 + docs/branching.svg.license | 2 + docs/gplayDebugBuildVariant.png | Bin 0 -> 12930 bytes docs/gplayDebugBuildVariant.png.license | 2 + ...rantNotificationPermissionAfterInstall.png | Bin 0 -> 12356 bytes ...ficationPermissionAfterInstall.png.license | 2 + docs/ignoreBatteryOptimizationDialog.png | Bin 0 -> 46956 bytes ...gnoreBatteryOptimizationDialog.png.license | 2 + ...ignoreBatteryOptimizationSelectAllApps.png | Bin 0 -> 37000 bytes ...tteryOptimizationSelectAllApps.png.license | 2 + ...ignoreBatteryOptimizationTurnOffSwitch.png | Bin 0 -> 30905 bytes ...tteryOptimizationTurnOffSwitch.png.license | 2 + docs/notificationSettingsExample.png | Bin 0 -> 53839 bytes docs/notificationSettingsExample.png.license | 2 + docs/notifications.md | 160 + .../notificationsNotSetUpCorrectlyWarning.png | Bin 0 -> 40482 bytes ...ationsNotSetUpCorrectlyWarning.png.license | 2 + docs/semantic_versioning_code.png | Bin 0 -> 5433 bytes docs/semantic_versioning_code.png.license | 2 + drawable_resources/icon-background.svg | 4 + .../icon-background.svg.license | 2 + drawable_resources/icon-foreground.svg | 6 + .../icon-foreground.svg.license | 2 + drawable_resources/icon-foreground_qa.svg | 5 + .../icon-foreground_qa.svg.license | 2 + drawable_resources/other/account-circle.svg | 7 + .../other/account-circle.svg.license | 2 + .../other/circular_document.svg | 10 + .../other/circular_document.svg.license | 3 + drawable_resources/other/circular_group.svg | 10 + .../other/circular_group.svg.license | 3 + drawable_resources/other/circular_link.svg | 10 + .../other/circular_link.svg.license | 3 + .../other/circular_location.svg | 10 + .../other/circular_location.svg.license | 3 + drawable_resources/other/circular_lock.svg | 10 + .../other/circular_lock.svg.license | 3 + drawable_resources/other/circular_mail.svg | 10 + .../other/circular_mail.svg.license | 3 + .../other/file-icon-black-24h.svg | 11 + .../other/file-icon-black-24h.svg.license | 2 + drawable_resources/other/file-icon.svg | 12 + .../other/file-icon.svg.license | 2 + .../other/file-password-request.svg | 9 + .../other/file-password-request.svg.license | 2 + drawable_resources/other/ic_crop_16_9.svg | 5 + .../other/ic_crop_16_9.svg.license | 3 + drawable_resources/other/ic_crop_4_3.svg | 5 + .../other/ic_crop_4_3.svg.license | 3 + drawable_resources/other/ic_low_quality.svg | 5 + .../other/ic_low_quality.svg.license | 3 + fastlane/Appfile | 2 + fastlane/Appfile.license | 2 + fastlane/Fastfile | 22 + .../android/cs-CZ/full_description.txt | 25 + .../android/cs-CZ/short_description.txt | 1 + fastlane/metadata/android/cs-CZ/title.txt | 1 + .../android/de-DE/full_description.txt | 25 + .../android/de-DE/short_description.txt | 1 + fastlane/metadata/android/de-DE/title.txt | 1 + .../android/el-GR/short_description.txt | 1 + fastlane/metadata/android/el-GR/title.txt | 1 + .../android/en-US/full_description.txt | 25 + .../android/en-US/images/featureGraphic.png | Bin 0 -> 434972 bytes .../metadata/android/en-US/images/icon.png | Bin 0 -> 129190 bytes .../conversationList_dark.png | Bin 0 -> 227288 bytes .../conversationList_light.png | Bin 0 -> 227596 bytes .../phoneScreenshots/markdown_light.png | Bin 0 -> 142940 bytes .../searchParticipant_light.png | Bin 0 -> 60472 bytes .../phoneScreenshots/serverSelection.png | Bin 0 -> 56700 bytes .../phoneScreenshots/settings_light.png | Bin 0 -> 137878 bytes .../images/phoneScreenshots/voiceCall.png | Bin 0 -> 148609 bytes .../phoneScreenshots/voiceRecord_light.png | Bin 0 -> 370254 bytes .../android/en-US/short_description.txt | 1 + fastlane/metadata/android/en-US/title.txt | 1 + .../android/es-ES/full_description.txt | 25 + .../android/es-ES/short_description.txt | 1 + fastlane/metadata/android/es-ES/title.txt | 1 + .../android/fr-FR/full_description.txt | 25 + .../android/fr-FR/short_description.txt | 1 + fastlane/metadata/android/fr-FR/title.txt | 1 + .../android/is-IS/short_description.txt | 1 + fastlane/metadata/android/is-IS/title.txt | 1 + .../android/it-IT/full_description.txt | 25 + .../android/it-IT/short_description.txt | 1 + fastlane/metadata/android/it-IT/title.txt | 1 + .../android/nl-NL/full_description.txt | 25 + .../android/nl-NL/short_description.txt | 1 + fastlane/metadata/android/nl-NL/title.txt | 1 + .../android/pl-PL/short_description.txt | 1 + fastlane/metadata/android/pl-PL/title.txt | 1 + .../android/pt-BR/full_description.txt | 25 + .../android/pt-BR/short_description.txt | 1 + fastlane/metadata/android/pt-BR/title.txt | 1 + .../android/ru-RU/full_description.txt | 25 + .../android/ru-RU/short_description.txt | 1 + fastlane/metadata/android/ru-RU/title.txt | 1 + .../android/sr-SR/full_description.txt | 26 + .../android/sr-SR/short_description.txt | 1 + fastlane/metadata/android/sr-SR/title.txt | 1 + .../android/tr-TR/full_description.txt | 27 + .../android/tr-TR/short_description.txt | 1 + fastlane/metadata/android/tr-TR/title.txt | 1 + .../android/vi-VI/short_description.txt | 1 + fastlane/metadata/android/vi-VI/title.txt | 1 + gradle.properties | 35 + gradle/verification-keyring.keys | 6933 +++++ gradle/verification-keyring.keys.license | 2 + gradle/verification-metadata.xml | 22485 ++++++++++++++++ gradle/verification-metadata.xml.license | 2 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45457 bytes gradle/wrapper/gradle-wrapper.jar.license | 2 + gradle/wrapper/gradle-wrapper.properties | 7 + .../wrapper/gradle-wrapper.properties.license | 2 + gradlew | 251 + gradlew.bat | 94 + gradlew.bat.license | 2 + gradlew.license | 2 + renovate.json5 | 19 + renovate.json5.license | 2 + scripts/analysis/analysis-wrapper.sh | 148 + scripts/analysis/getBranchName.sh | 13 + scripts/analysis/lint-results.txt | 2 + scripts/analysis/lint-results.txt.license | 2 + scripts/analysis/lint-up.rb | 191 + scripts/analysis/spotbugs-up.rb | 53 + scripts/analysis/spotbugsComparison.py | 56 + scripts/analysis/spotbugsSummary.py | 64 + scripts/checkGplayLimitation.sh | 18 + scripts/hooks/pre-commit | 19 + scripts/hooks/pre-push | 32 + scripts/lib.sh | 27 + scripts/metadata/generate_metadata.py | 127 + scripts/repo | 1 + scripts/repo.license | 2 + scripts/tools/OxygenConversion.MD | 11 + scripts/wait_for_emulator.sh | 29 + settings.gradle | 22 + spotbugs-filter.xml | 93 + 1403 files changed, 189511 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENCE.md create mode 100644 LICENSES/AGPL-3.0-or-later.txt create mode 100644 LICENSES/Apache-2.0.txt create mode 100644 LICENSES/CC-BY-4.0.txt create mode 100644 LICENSES/CC-BY-SA-3.0.txt create mode 100644 LICENSES/CC0-1.0.txt create mode 100644 LICENSES/GPL-2.0-only.txt create mode 100644 LICENSES/GPL-3.0-or-later.txt create mode 100644 LICENSES/LGPL-2.1-or-later.txt create mode 100644 LICENSES/LGPL-3.0-or-later.txt create mode 100644 LICENSES/LicenseRef-NextcloudTrademarks.txt create mode 100644 LICENSES/LicenseRef-XTrademarks.txt create mode 100644 LICENSES/MIT.txt create mode 100644 README.md create mode 100644 REUSE.toml create mode 100644 SECURITY.md create mode 100644 SETUP.md create mode 100644 app/build.gradle create mode 100644 app/lint.xml create mode 100644 app/proguard-rules.pro create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/11.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/12.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/14.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/15.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/16.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/17.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/18.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/19.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/20.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/21.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/8.json create mode 100644 app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/9.json create mode 100644 app/src/androidTest/java/com/nextcloud/talk/ExampleInstrumentedTest.java create mode 100644 app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt create mode 100644 app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt create mode 100644 app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt create mode 100644 app/src/androidTest/java/com/nextcloud/talk/data/database/migrations/MigrationsTest.kt create mode 100644 app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java create mode 100644 app/src/androidTest/java/com/nextcloud/talk/utils/ColorGeneratorTest.kt create mode 100644 app/src/androidTest/java/com/nextcloud/talk/utils/ShareUtilsIT.kt create mode 100644 app/src/androidTest/java/com/nextcloud/talk/utils/UriUtilsIT.kt create mode 100644 app/src/androidTest/java/com/nextcloud/talk/utils/VibrationUtilsTest.kt create mode 100644 app/src/generic/fastlane/metadata/android/tr-TR/full_description.txt create mode 100644 app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java create mode 100644 app/src/gplay/AndroidManifest.xml create mode 100644 app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt create mode 100644 app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt create mode 100644 app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/leafletMapMessagePreview.html create mode 100644 app/src/main/assets/leafletMapMessagePreview.html.license create mode 100644 app/src/main/ic_launcher-web.png create mode 100644 app/src/main/java/com/nextcloud/talk/PhoneUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt create mode 100644 app/src/main/java/com/nextcloud/talk/account/data/model/LoginResponseModels.kt create mode 100644 app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt create mode 100644 app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/activities/ActionBarProvider.java create mode 100644 app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/activities/CallBaseActivity.java create mode 100644 app/src/main/java/com/nextcloud/talk/activities/CallStatus.kt create mode 100644 app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/activities/TakePhotoActivity.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItemNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusClickListener.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusListAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/ReactionItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/ReactionItemClickListener.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/FlexibleItemViewType.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/MessagesTextHeaderItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/NotificationSoundItem.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/SpacerItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/AdjustableMessageHolderInterface.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedMessageInterface.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/MessagePayload.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingDeckCardViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageInterface.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/Thread.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/ThreadButton.kt create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/UnreadNoticeMessageViewHolder.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt create mode 100644 app/src/main/java/com/nextcloud/talk/api/NcApi.java create mode 100644 app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt create mode 100644 app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt create mode 100644 app/src/main/java/com/nextcloud/talk/arbitrarystorage/ArbitraryStorageManager.kt create mode 100644 app/src/main/java/com/nextcloud/talk/bottomsheet/items/BasicListItemWithImage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/bottomsheet/items/BottomSheets.kt create mode 100644 app/src/main/java/com/nextcloud/talk/bottomsheet/items/ListIconDialogAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/CallParticipant.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/CallParticipantList.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/CallParticipantListNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/CallParticipantModel.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/CallParticipantModelNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/LocalCallParticipantModel.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/LocalCallParticipantModelNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcaster.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcasterMcu.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcasterNoMcu.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/MessageSender.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/MutableCallParticipantModel.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/MutableLocalCallParticipantModel.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/RaisedHand.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt create mode 100644 app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java create mode 100644 app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/OverflowMenu.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/TypingParticipant.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/io/AudioFocusRequestManager.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/io/AudioRecorderManager.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/io/LifecycleAwareManager.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/io/MediaRecorderManager.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/components/ColoredStatusBar.kt create mode 100644 app/src/main/java/com/nextcloud/talk/components/StandardAppBar.kt create mode 100644 app/src/main/java/com/nextcloud/talk/components/VerticallyCenteredRow.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsApplication.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ImageRequest.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/components/ContactsAppBar.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/components/ContactsItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/components/ContactsList.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/components/ContactsSearchAppBar.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt create mode 100644 app/src/main/java/com/nextcloud/talk/contacts/components/Header.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfo/CreateRoomRequest.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfo/Participants.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationlist/data/network/ConversationsNetworkDataSource.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationlist/data/network/RetrofitConversationsNetwork.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/dagger/modules/BusModule.java create mode 100644 app/src/main/java/com/nextcloud/talk/dagger/modules/ContextModule.java create mode 100644 app/src/main/java/com/nextcloud/talk/dagger/modules/DaosModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java create mode 100644 app/src/main/java/com/nextcloud/talk/dagger/modules/ManagerModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java create mode 100644 app/src/main/java/com/nextcloud/talk/dagger/modules/UtilsModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/database/model/SendStatus.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitor.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitorImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/converters/ArrayListConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/converters/CapabilitiesConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/converters/ExternalSignalingServerConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapStringIntConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/converters/PushConfigurationConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/converters/SendStatusConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/converters/ServerVersionConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/source/local/converters/SignalingSettingsConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStorageMapper.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesDao.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorageEntity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/user/model/User.kt create mode 100644 app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseContentComposable.kt create mode 100644 app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/events/CertificateEvent.java create mode 100644 app/src/main/java/com/nextcloud/talk/events/ConfigurationChangeEvent.java create mode 100644 app/src/main/java/com/nextcloud/talk/events/ConversationsListFetchDataEvent.kt create mode 100644 app/src/main/java/com/nextcloud/talk/events/EventStatus.java create mode 100644 app/src/main/java/com/nextcloud/talk/events/MoreMenuClickEvent.java create mode 100644 app/src/main/java/com/nextcloud/talk/events/NetworkEvent.java create mode 100644 app/src/main/java/com/nextcloud/talk/events/OpenConversationEvent.kt create mode 100644 app/src/main/java/com/nextcloud/talk/events/ProximitySensorEvent.java create mode 100644 app/src/main/java/com/nextcloud/talk/events/UserMentionClickEvent.java create mode 100644 app/src/main/java/com/nextcloud/talk/events/WebSocketCommunicationEvent.java create mode 100644 app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt create mode 100644 app/src/main/java/com/nextcloud/talk/extensions/LongFormatExtension.kt create mode 100644 app/src/main/java/com/nextcloud/talk/extensions/ParcelableExtensions.kt create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/models/BrowserFile.kt create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/models/DavResponse.java create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCEncrypted.kt create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCPermission.kt create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCPreview.kt create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCFavorite.kt create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCId.kt create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCSize.kt create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/webdav/DavUtils.java create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/webdav/ReadFilesystemOperation.java create mode 100644 app/src/main/java/com/nextcloud/talk/filebrowser/webdav/ReadFolderListingOperation.kt create mode 100644 app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenImageActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/interfaces/ClosedInterface.kt create mode 100644 app/src/main/java/com/nextcloud/talk/interfaces/ConversationMenuInterface.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/data/Invitation.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/data/InvitationActionModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversationWorker.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesWorker.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.java create mode 100644 app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/location/GeocodingResult.kt create mode 100644 app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/lock/LockedActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchHelper.kt create mode 100644 app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/ImportAccount.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/LoginData.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/MessageDraft.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/RetrofitBucket.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/RingtoneSettings.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/SignatureVerification.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/TakePictureViewModel.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/domain/ReactionAddedModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/domain/ReactionDeletedModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/domain/StartCallRecordingModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/domain/StopCallRecordingModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/domain/converters/DomainEnumNotificationLevelConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/AnyParceler.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteUser.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/CoreCapability.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/NotificationsCapability.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/ServerVersion.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/ThemingCapability.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverallSingleMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverviewOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverviewOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomsOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomsOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordData.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/EnumActorTypeConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/EnumLobbyStateConverter.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/EnumNotificationLevelConverter.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/EnumParticipantTypeConverter.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReactionActorTypeConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReadOnlyConversationConverter.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/EnumRoomTypeConverter.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/LoganSquareJodaTimeConverter.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/ObjectParcelConverter.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/ScopeConverter.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/converters/UriTypeConverter.java create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/generic/GenericMeta.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/generic/GenericOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/generic/GenericOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/generic/Status.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCard.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardAction.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/invitation/Invitation.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/mention/MentionOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/mention/MentionOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/notifications/Notification.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationAction.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationRichObject.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationsOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationsOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphObject.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphResponse.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/opengraph/Reference.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/opengraph/RichObject.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/AddParticipantOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/AddParticipantOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/ParticipantsOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/ParticipantsOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBan.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/profile/CoreProfileAction.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/profile/Profile.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/push/NotificationUser.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/push/PushConfigurationState.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistration.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionVoter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/reminder/Reminder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/reminder/ReminderOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/reminder/ReminderOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/sharees/ExactSharees.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/sharees/Sharee.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/sharees/ShareesOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/sharees/ShareesOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/sharees/SharesData.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/sharees/Value.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/DataChannelMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/NCIceCandidate.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessagePayload.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/NCSignalingMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/Signaling.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationHelloAuthParams.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationSettings.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettings.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/status/ClearAt.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/status/Status.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/status/StatusOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/status/StatusOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/status/StatusType.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatus.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationData.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/threads/Thread.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadAttendee.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadInfo.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadsOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadsOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchEntry.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchResponseData.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceData.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userprofile/Scope.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileData.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/ActorWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/AuthParametersWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/AuthWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/ByeWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/CallOverallWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/CallWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorOverallWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/EventOverallWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloOverallWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseOverallWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/JoinedRoomOverallWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomFederationWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomOverallWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomPropertiesWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/websocket/ServerHelloResponseFeaturesWebSocketMessage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/openconversations/adapters/OpenConversationsAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/openconversations/viewmodels/OpenConversationsViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsItemListener.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/model/PollDetails.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollDetailsResponse.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java create mode 100644 app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/raisehand/WithdrawRequestAssistanceModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/raisehand/viewmodel/RaiseHandViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt create mode 100644 app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt create mode 100644 app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt create mode 100644 app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt create mode 100644 app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt create mode 100644 app/src/main/java/com/nextcloud/talk/remotefilebrowser/SelectionInterface.kt create mode 100644 app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/remotefilebrowser/model/RemoteFileBrowserItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/model/SharedDeckCardItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/model/SharedFileItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItemType.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItems.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/model/SharedLocationItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/model/SharedOtherItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/model/SharedPollItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/signaling/CallParticipantMessageNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt create mode 100644 app/src/main/java/com/nextcloud/talk/signaling/LocalParticipantMessageNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/signaling/OfferMessageNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/signaling/ParticipantListMessageNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java create mode 100644 app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageSender.java create mode 100644 app/src/main/java/com/nextcloud/talk/signaling/WebRtcMessageNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/threadsoverview/components/ThreadRow.kt create mode 100644 app/src/main/java/com/nextcloud/talk/threadsoverview/data/ThreadsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/threadsoverview/data/ThreadsRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/threadsoverview/viewmodels/ThreadsOverviewViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/repositories/model/Language.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesData.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateData.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslationsOverall.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/BackgroundVoiceMessageCard.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/MicInputCloud.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/StatusDrawable.java create mode 100644 app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/ContextChatCompose.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimeCompose.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/FileAttachmentPreviewFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/OnlineStatusBottomDialogFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/SetPhoneNumberDialogFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/StatusMessageBottomDialogFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/recyclerview/MessageSwipeActions.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/recyclerview/MessageSwipeCallback.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProvider.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/theme/ServerThemeImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/upload/chunked/Chunk.kt create mode 100644 app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkFromFileRequestBody.kt create mode 100644 app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt create mode 100644 app/src/main/java/com/nextcloud/talk/upload/chunked/OnDataTransferProgressListener.kt create mode 100644 app/src/main/java/com/nextcloud/talk/upload/normal/FileUploader.kt create mode 100644 app/src/main/java/com/nextcloud/talk/users/UserManager.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/AppCompatActivityExtensions.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/AudioUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/AuthenticatorService.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/BrandingUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/CharPolicy.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ChatMessageUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ContextExtensions.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/DateConstants.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/DeviceUtils.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/DoNotDisturbUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/DrawableUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/EmojiTextInputEditText.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/FABAwareScrollingViewBehavior.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDate.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySize.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ImageEmojiEditText.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/Mimetype.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/MimetypeUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/NoSupportedApiException.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/PickImage.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ReceiverFlag.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/RemoteFileUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/SecurityUtils.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ShareUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/SyncAdapter.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/SyncService.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/TextDrawable.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/TextMatchers.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/UserIdUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/VibrationUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/animations/PulseAnimation.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/animations/ViewHidingBehaviourAnimation.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageModule.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderNew.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/message/SendMessageUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtil.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/power/ProximityLock.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtils.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/rx/DisposableSet.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/rx/SearchViewObservable.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideMessageHolder.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/singletons/AvatarStatusCodeHolder.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ssl/KeyManager.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ssl/TrustManager.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/text/Spans.java create mode 100644 app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/viewmodels/GeoCodingViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/webrtc/DataChannelMessageNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/webrtc/Globals.java create mode 100644 app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionNotifier.java create mode 100644 app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java create mode 100644 app/src/main/java/com/nextcloud/talk/webrtc/ProximitySensor.java create mode 100644 app/src/main/java/com/nextcloud/talk/webrtc/WebRTCUtils.java create mode 100644 app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java create mode 100644 app/src/main/java/com/nextcloud/talk/webrtc/WebRtcBluetoothManager.java create mode 100644 app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java create mode 100644 app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt create mode 100644 app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java create mode 100644 app/src/main/java/third/parties/daveKoeller/AlphanumComparator.java create mode 100644 app/src/main/java/third/parties/fresco/BetterImageSpan.kt create mode 100644 app/src/main/res/anim/popup_animation.xml create mode 100644 app/src/main/res/animator/appbar_elevation_off.xml create mode 100644 app/src/main/res/animator/appbar_elevation_on.xml create mode 100644 app/src/main/res/drawable-land/link_text_background.xml create mode 100644 app/src/main/res/drawable-night/account_circle_48dp.xml create mode 100644 app/src/main/res/drawable-night/account_circle_96dp.xml create mode 100644 app/src/main/res/drawable-night/ic_cellphone.xml create mode 100644 app/src/main/res/drawable-night/ic_chevron_right.xml create mode 100644 app/src/main/res/drawable-night/ic_circular_document.xml create mode 100644 app/src/main/res/drawable-night/ic_circular_group.xml create mode 100644 app/src/main/res/drawable-night/ic_circular_group_mentions.xml create mode 100644 app/src/main/res/drawable-night/ic_circular_link.xml create mode 100644 app/src/main/res/drawable-night/ic_circular_location.xml create mode 100644 app/src/main/res/drawable-night/ic_circular_lock.xml create mode 100644 app/src/main/res/drawable-night/ic_circular_mail.xml create mode 100644 app/src/main/res/drawable-night/ic_close_search.xml create mode 100644 app/src/main/res/drawable-night/ic_contacts.xml create mode 100644 app/src/main/res/drawable-night/ic_link.xml create mode 100644 app/src/main/res/drawable-night/ic_password.xml create mode 100644 app/src/main/res/drawable-night/icon_circular_phone.xml create mode 100644 app/src/main/res/drawable-night/icon_circular_team.xml create mode 100644 app/src/main/res/drawable/accent_circle.xml create mode 100644 app/src/main/res/drawable/account_circle_48dp.xml create mode 100644 app/src/main/res/drawable/account_circle_96dp.xml create mode 100644 app/src/main/res/drawable/baseline_article_24.xml create mode 100644 app/src/main/res/drawable/baseline_audiotrack_24.xml create mode 100644 app/src/main/res/drawable/baseline_bar_chart_24.xml create mode 100644 app/src/main/res/drawable/baseline_block_24.xml create mode 100644 app/src/main/res/drawable/baseline_calendar_month_24.xml create mode 100644 app/src/main/res/drawable/baseline_calendar_today_24.xml create mode 100644 app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml create mode 100644 app/src/main/res/drawable/baseline_contacts_24.xml create mode 100644 app/src/main/res/drawable/baseline_download_24.xml create mode 100644 app/src/main/res/drawable/baseline_error_24.xml create mode 100644 app/src/main/res/drawable/baseline_error_outline_24.xml create mode 100644 app/src/main/res/drawable/baseline_format_list_bulleted_24.xml create mode 100644 app/src/main/res/drawable/baseline_image_24.xml create mode 100644 app/src/main/res/drawable/baseline_info_24.xml create mode 100644 app/src/main/res/drawable/baseline_insert_drive_file_24.xml create mode 100644 app/src/main/res/drawable/baseline_location_pin_24.xml create mode 100644 app/src/main/res/drawable/baseline_lock_open_24.xml create mode 100644 app/src/main/res/drawable/baseline_mic_24.xml create mode 100644 app/src/main/res/drawable/baseline_notifications_24.xml create mode 100644 app/src/main/res/drawable/baseline_photo_library_24.xml create mode 100644 app/src/main/res/drawable/baseline_report_problem_24.xml create mode 100644 app/src/main/res/drawable/baseline_schedule_24.xml create mode 100644 app/src/main/res/drawable/baseline_stop_24.xml create mode 100644 app/src/main/res/drawable/baseline_tag_faces_24.xml create mode 100644 app/src/main/res/drawable/baseline_unfold_less_24.xml create mode 100644 app/src/main/res/drawable/baseline_unfold_more_24.xml create mode 100644 app/src/main/res/drawable/baseline_video_24.xml create mode 100644 app/src/main/res/drawable/borderless_btn.xml create mode 100644 app/src/main/res/drawable/current_location_circle.xml create mode 100644 app/src/main/res/drawable/cutout_circle.xml create mode 100644 app/src/main/res/drawable/deck.xml create mode 100644 app/src/main/res/drawable/forward_24.xml create mode 100644 app/src/main/res/drawable/ic_account_plus.xml create mode 100644 app/src/main/res/drawable/ic_add_grey600_24px.xml create mode 100644 app/src/main/res/drawable/ic_alphabetical_asc.xml create mode 100644 app/src/main/res/drawable/ic_alphabetical_desc.xml create mode 100644 app/src/main/res/drawable/ic_arrow_back_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_arrow_forward_white_24px.xml create mode 100644 app/src/main/res/drawable/ic_av_timer_timer_24dp.xml create mode 100644 app/src/main/res/drawable/ic_avatar_background.xml create mode 100644 app/src/main/res/drawable/ic_avatar_document.xml create mode 100644 app/src/main/res/drawable/ic_avatar_federation.xml create mode 100644 app/src/main/res/drawable/ic_avatar_group.xml create mode 100644 app/src/main/res/drawable/ic_avatar_group_small.xml create mode 100644 app/src/main/res/drawable/ic_avatar_link.xml create mode 100644 app/src/main/res/drawable/ic_avatar_mail.xml create mode 100644 app/src/main/res/drawable/ic_avatar_team_small.xml create mode 100644 app/src/main/res/drawable/ic_baseline_arrow_downward_24px.xml create mode 100644 app/src/main/res/drawable/ic_baseline_attach_file_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_bar_chart_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_bluetooth_audio_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_close_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_deck_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_do_not_touch_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_error_outline_24dp.xml create mode 100644 app/src/main/res/drawable/ic_baseline_filter_list_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_flash_off_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_flash_on_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_flip_camera_android_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_headset_mic_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_keyboard_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_location_on_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_location_on_red_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_mic_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_mic_red_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_notifications_off_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_pause_voice_message_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_person_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_phone_in_talk_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_phone_missed_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_photo_camera_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_picture_in_picture_alt_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_play_arrow_voice_message_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_translate_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_videocam_24.xml create mode 100644 app/src/main/res/drawable/ic_call_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_call_end_white_24px.xml create mode 100644 app/src/main/res/drawable/ic_call_grey_600_24dp.xml create mode 100644 app/src/main/res/drawable/ic_call_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_cancel_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_cancel_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_cellphone.xml create mode 100644 app/src/main/res/drawable/ic_check.xml create mode 100644 app/src/main/res/drawable/ic_check_24.xml create mode 100644 app/src/main/res/drawable/ic_check_all.xml create mode 100644 app/src/main/res/drawable/ic_check_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_check_circle.xml create mode 100644 app/src/main/res/drawable/ic_check_circle_outlined.xml create mode 100644 app/src/main/res/drawable/ic_chevron_right.xml create mode 100644 app/src/main/res/drawable/ic_circular_document.xml create mode 100644 app/src/main/res/drawable/ic_circular_group.xml create mode 100644 app/src/main/res/drawable/ic_circular_group_mentions.xml create mode 100644 app/src/main/res/drawable/ic_circular_link.xml create mode 100644 app/src/main/res/drawable/ic_circular_location.xml create mode 100644 app/src/main/res/drawable/ic_circular_lock.xml create mode 100644 app/src/main/res/drawable/ic_circular_mail.xml create mode 100644 app/src/main/res/drawable/ic_clear_24.xml create mode 100644 app/src/main/res/drawable/ic_close_search.xml create mode 100644 app/src/main/res/drawable/ic_comment.xml create mode 100644 app/src/main/res/drawable/ic_contacts.xml create mode 100644 app/src/main/res/drawable/ic_content_copy.xml create mode 100644 app/src/main/res/drawable/ic_crop_16_9.xml create mode 100644 app/src/main/res/drawable/ic_crop_4_3.xml create mode 100644 app/src/main/res/drawable/ic_delete.xml create mode 100644 app/src/main/res/drawable/ic_delete_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_delete_grey600_24dp.xml create mode 100644 app/src/main/res/drawable/ic_dots_horizontal.xml create mode 100644 app/src/main/res/drawable/ic_dots_horizontal_white.xml create mode 100644 app/src/main/res/drawable/ic_edit.xml create mode 100644 app/src/main/res/drawable/ic_edit_24.xml create mode 100644 app/src/main/res/drawable/ic_edit_note_24.xml create mode 100644 app/src/main/res/drawable/ic_email.xml create mode 100644 app/src/main/res/drawable/ic_exit_to_app_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_eye.xml create mode 100644 app/src/main/res/drawable/ic_eye_off.xml create mode 100644 app/src/main/res/drawable/ic_folder.xml create mode 100644 app/src/main/res/drawable/ic_folder_multiple_image.xml create mode 100644 app/src/main/res/drawable/ic_hand_back_left.xml create mode 100644 app/src/main/res/drawable/ic_high_quality.xml create mode 100644 app/src/main/res/drawable/ic_info_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_insert_emoticon_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_arrow_down.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_arrow_up.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_double_arrow_down.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_link.xml create mode 100644 app/src/main/res/drawable/ic_link_black_24px.xml create mode 100644 app/src/main/res/drawable/ic_link_grey600_24px.xml create mode 100644 app/src/main/res/drawable/ic_list_empty_error.xml create mode 100644 app/src/main/res/drawable/ic_lock_grey600_24px.xml create mode 100644 app/src/main/res/drawable/ic_lock_open_grey600_24dp.xml create mode 100644 app/src/main/res/drawable/ic_lock_plus_grey600_24dp.xml create mode 100644 app/src/main/res/drawable/ic_lock_white_24px.xml create mode 100644 app/src/main/res/drawable/ic_logo.xml create mode 100644 app/src/main/res/drawable/ic_low_quality.xml create mode 100644 app/src/main/res/drawable/ic_map_marker.xml create mode 100644 app/src/main/res/drawable/ic_menu.xml create mode 100644 app/src/main/res/drawable/ic_mic_grey_600_24dp.xml create mode 100644 app/src/main/res/drawable/ic_mic_off_white_24px.xml create mode 100644 app/src/main/res/drawable/ic_mic_white_24px.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_application_pdf.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_audio.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_file.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_folder.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_folder_shared.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_image.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_link.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_location.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_package_x_generic.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_text.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_text_calendar.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_text_code.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_text_vcard.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_video.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_x_office_document.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_x_office_presentation.xml create mode 100755 app/src/main/res/drawable/ic_mimetype_x_office_spreadsheet.xml create mode 100644 app/src/main/res/drawable/ic_modification_asc.xml create mode 100644 app/src/main/res/drawable/ic_modification_desc.xml create mode 100644 app/src/main/res/drawable/ic_more_horiz_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_note_to_self.xml create mode 100644 app/src/main/res/drawable/ic_notification.xml create mode 100644 app/src/main/res/drawable/ic_password.xml create mode 100644 app/src/main/res/drawable/ic_pencil_grey600_24dp.xml create mode 100644 app/src/main/res/drawable/ic_people_group_black_24px.xml create mode 100644 app/src/main/res/drawable/ic_phone.xml create mode 100644 app/src/main/res/drawable/ic_phone_small.xml create mode 100644 app/src/main/res/drawable/ic_refresh.xml create mode 100644 app/src/main/res/drawable/ic_reply.xml create mode 100644 app/src/main/res/drawable/ic_room_service_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_search_grey.xml create mode 100644 app/src/main/res/drawable/ic_search_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_security_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/drawable/ic_share_action.xml create mode 100644 app/src/main/res/drawable/ic_share_variant.xml create mode 100644 app/src/main/res/drawable/ic_signal_wifi_off_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_size_asc.xml create mode 100644 app/src/main/res/drawable/ic_size_desc.xml create mode 100644 app/src/main/res/drawable/ic_star_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_star_border_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_switch_video_white_24px.xml create mode 100644 app/src/main/res/drawable/ic_talk.xml create mode 100644 app/src/main/res/drawable/ic_timer_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_twitter.xml create mode 100644 app/src/main/res/drawable/ic_user.xml create mode 100644 app/src/main/res/drawable/ic_user_status_away.xml create mode 100644 app/src/main/res/drawable/ic_user_status_busy.xml create mode 100644 app/src/main/res/drawable/ic_user_status_dnd.xml create mode 100644 app/src/main/res/drawable/ic_user_status_invisible.xml create mode 100644 app/src/main/res/drawable/ic_videocam_grey_600_24dp.xml create mode 100644 app/src/main/res/drawable/ic_videocam_off_white_24px.xml create mode 100644 app/src/main/res/drawable/ic_videocam_white_24px.xml create mode 100644 app/src/main/res/drawable/ic_volume_mute_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_volume_up_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_warning_white.xml create mode 100644 app/src/main/res/drawable/ic_web.xml create mode 100644 app/src/main/res/drawable/icon_circular_phone.xml create mode 100644 app/src/main/res/drawable/icon_circular_team.xml create mode 100644 app/src/main/res/drawable/incoming_gradient.xml create mode 100644 app/src/main/res/drawable/launch_screen.xml create mode 100644 app/src/main/res/drawable/link_text_background.xml create mode 100644 app/src/main/res/drawable/link_text_no_preview_background.xml create mode 100644 app/src/main/res/drawable/mention_chip.xml create mode 100644 app/src/main/res/drawable/online_status.xml create mode 100644 app/src/main/res/drawable/outline_archive_24.xml create mode 100644 app/src/main/res/drawable/outline_forum_24.xml create mode 100644 app/src/main/res/drawable/outline_notifications_active_24.xml create mode 100644 app/src/main/res/drawable/outline_qr_code_24.xml create mode 100644 app/src/main/res/drawable/reaction_self_background.xml create mode 100644 app/src/main/res/drawable/reaction_self_bottom_sheet_background.xml create mode 100644 app/src/main/res/drawable/record_start.xml create mode 100644 app/src/main/res/drawable/record_starting.xml create mode 100644 app/src/main/res/drawable/record_stop.xml create mode 100644 app/src/main/res/drawable/reply_background.xml create mode 100644 app/src/main/res/drawable/round_bgnd.xml create mode 100644 app/src/main/res/drawable/shape_grouped_incoming_message.xml create mode 100644 app/src/main/res/drawable/shape_grouped_outcoming_message.xml create mode 100644 app/src/main/res/drawable/shape_incoming_message.xml create mode 100644 app/src/main/res/drawable/shape_outcoming_message.xml create mode 100644 app/src/main/res/drawable/shape_oval.xml create mode 100644 app/src/main/res/drawable/trashbin.xml create mode 100644 app/src/main/res/drawable/upload.xml create mode 100644 app/src/main/res/drawable/upload_white.xml create mode 100644 app/src/main/res/drawable/voice_message_outgoing_seek_bar_slider.xml create mode 100644 app/src/main/res/layout-land/activity_profile.xml create mode 100644 app/src/main/res/layout-land/activity_translate.xml create mode 100644 app/src/main/res/layout-land/reference_inside_message.xml create mode 100644 app/src/main/res/layout/account_item.xml create mode 100644 app/src/main/res/layout/activity_account_verification.xml create mode 100644 app/src/main/res/layout/activity_chat.xml create mode 100644 app/src/main/res/layout/activity_conversation_info.xml create mode 100644 app/src/main/res/layout/activity_conversation_info_edit.xml create mode 100644 app/src/main/res/layout/activity_conversations.xml create mode 100644 app/src/main/res/layout/activity_full_screen_image.xml create mode 100644 app/src/main/res/layout/activity_full_screen_media.xml create mode 100644 app/src/main/res/layout/activity_full_screen_text.xml create mode 100644 app/src/main/res/layout/activity_geocoding.xml create mode 100644 app/src/main/res/layout/activity_invitations.xml create mode 100644 app/src/main/res/layout/activity_location.xml create mode 100644 app/src/main/res/layout/activity_locked.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_message_search.xml create mode 100644 app/src/main/res/layout/activity_open_conversations.xml create mode 100644 app/src/main/res/layout/activity_profile.xml create mode 100644 app/src/main/res/layout/activity_remote_file_browser.xml create mode 100644 app/src/main/res/layout/activity_server_selection.xml create mode 100644 app/src/main/res/layout/activity_settings.xml create mode 100644 app/src/main/res/layout/activity_shared_items.xml create mode 100644 app/src/main/res/layout/activity_switch_account.xml create mode 100644 app/src/main/res/layout/activity_take_picture.xml create mode 100644 app/src/main/res/layout/activity_translate.xml create mode 100644 app/src/main/res/layout/activity_web_view_login.xml create mode 100644 app/src/main/res/layout/ban_item_list.xml create mode 100644 app/src/main/res/layout/call_activity.xml create mode 100644 app/src/main/res/layout/call_notification_activity.xml create mode 100644 app/src/main/res/layout/call_started_message.xml create mode 100644 app/src/main/res/layout/call_states.xml create mode 100644 app/src/main/res/layout/create_thread_view.xml create mode 100644 app/src/main/res/layout/current_account_item.xml create mode 100644 app/src/main/res/layout/dialog_attachment.xml create mode 100644 app/src/main/res/layout/dialog_audio_output.xml create mode 100644 app/src/main/res/layout/dialog_ban_participant.xml create mode 100644 app/src/main/res/layout/dialog_choose_account.xml create mode 100644 app/src/main/res/layout/dialog_choose_account_share_to.xml create mode 100644 app/src/main/res/layout/dialog_conversation_operations.xml create mode 100644 app/src/main/res/layout/dialog_create_conversation.xml create mode 100644 app/src/main/res/layout/dialog_file_attachment_preview.xml create mode 100644 app/src/main/res/layout/dialog_filter_conversation.xml create mode 100644 app/src/main/res/layout/dialog_message_actions.xml create mode 100644 app/src/main/res/layout/dialog_message_reactions.xml create mode 100644 app/src/main/res/layout/dialog_more_call_actions.xml create mode 100644 app/src/main/res/layout/dialog_password.xml create mode 100644 app/src/main/res/layout/dialog_poll_create.xml create mode 100644 app/src/main/res/layout/dialog_poll_loading.xml create mode 100644 app/src/main/res/layout/dialog_poll_main.xml create mode 100644 app/src/main/res/layout/dialog_poll_results.xml create mode 100644 app/src/main/res/layout/dialog_poll_vote.xml create mode 100644 app/src/main/res/layout/dialog_rename_conversation.xml create mode 100644 app/src/main/res/layout/dialog_scope.xml create mode 100644 app/src/main/res/layout/dialog_set_online_status.xml create mode 100644 app/src/main/res/layout/dialog_set_phone_number.xml create mode 100644 app/src/main/res/layout/dialog_set_status_message.xml create mode 100644 app/src/main/res/layout/dialog_temp_message_actions.xml create mode 100644 app/src/main/res/layout/edit_message_view.xml create mode 100644 app/src/main/res/layout/empty_list.xml create mode 100644 app/src/main/res/layout/federated_invitation_hint.xml create mode 100644 app/src/main/res/layout/fragment_dialog_ban_list.xml create mode 100644 app/src/main/res/layout/fragment_message_input.xml create mode 100644 app/src/main/res/layout/fragment_message_input_voice_recording.xml create mode 100644 app/src/main/res/layout/geocoding_item.xml create mode 100644 app/src/main/res/layout/item_custom_incoming_deck_card_message.xml create mode 100644 app/src/main/res/layout/item_custom_incoming_link_preview_message.xml create mode 100644 app/src/main/res/layout/item_custom_incoming_location_message.xml create mode 100644 app/src/main/res/layout/item_custom_incoming_poll_message.xml create mode 100644 app/src/main/res/layout/item_custom_incoming_preview_message.xml create mode 100644 app/src/main/res/layout/item_custom_incoming_text_message.xml create mode 100644 app/src/main/res/layout/item_custom_incoming_text_message_shimmer.xml create mode 100644 app/src/main/res/layout/item_custom_incoming_voice_message.xml create mode 100644 app/src/main/res/layout/item_custom_outcoming_deck_card_message.xml create mode 100644 app/src/main/res/layout/item_custom_outcoming_link_preview_message.xml create mode 100644 app/src/main/res/layout/item_custom_outcoming_location_message.xml create mode 100644 app/src/main/res/layout/item_custom_outcoming_poll_message.xml create mode 100644 app/src/main/res/layout/item_custom_outcoming_preview_message.xml create mode 100644 app/src/main/res/layout/item_custom_outcoming_text_message.xml create mode 100644 app/src/main/res/layout/item_custom_outcoming_voice_message.xml create mode 100644 app/src/main/res/layout/item_event_schedule.xml create mode 100644 app/src/main/res/layout/item_guest_access_settings.xml create mode 100644 app/src/main/res/layout/item_message_quote.xml create mode 100644 app/src/main/res/layout/item_notification_settings.xml create mode 100644 app/src/main/res/layout/item_reactions_tab.xml create mode 100644 app/src/main/res/layout/item_recording_consent.xml create mode 100644 app/src/main/res/layout/item_spacer.xml create mode 100644 app/src/main/res/layout/item_system_message.xml create mode 100644 app/src/main/res/layout/item_thread_title.xml create mode 100644 app/src/main/res/layout/item_webinar_info.xml create mode 100644 app/src/main/res/layout/lobby_view.xml create mode 100644 app/src/main/res/layout/menu_item_sheet.xml create mode 100644 app/src/main/res/layout/no_saved_messages_view.xml create mode 100644 app/src/main/res/layout/notifications_warning.xml create mode 100644 app/src/main/res/layout/out_of_office_view.xml create mode 100644 app/src/main/res/layout/poll_create_options_item.xml create mode 100644 app/src/main/res/layout/poll_result_header_item.xml create mode 100644 app/src/main/res/layout/poll_result_voter_item.xml create mode 100644 app/src/main/res/layout/poll_result_voters_overview_item.xml create mode 100644 app/src/main/res/layout/predefined_status.xml create mode 100644 app/src/main/res/layout/reaction_item.xml create mode 100644 app/src/main/res/layout/reactions_inside_message.xml create mode 100644 app/src/main/res/layout/reference_inside_message.xml create mode 100644 app/src/main/res/layout/remainder_to_delete_conversation.xml create mode 100644 app/src/main/res/layout/rv_item_browser_file.xml create mode 100644 app/src/main/res/layout/rv_item_contact.xml create mode 100644 app/src/main/res/layout/rv_item_conversation_info_participant.xml create mode 100644 app/src/main/res/layout/rv_item_conversation_with_last_message.xml create mode 100644 app/src/main/res/layout/rv_item_conversation_with_last_message_shimmer.xml create mode 100644 app/src/main/res/layout/rv_item_invitation.xml create mode 100644 app/src/main/res/layout/rv_item_load_more.xml create mode 100644 app/src/main/res/layout/rv_item_notification_sound.xml create mode 100644 app/src/main/res/layout/rv_item_open_conversation.xml create mode 100644 app/src/main/res/layout/rv_item_search_message.xml create mode 100644 app/src/main/res/layout/rv_item_title_header.xml create mode 100644 app/src/main/res/layout/search_layout.xml create mode 100644 app/src/main/res/layout/shared_item_grid.xml create mode 100644 app/src/main/res/layout/shared_item_list.xml create mode 100644 app/src/main/res/layout/sorting_order_fragment.xml create mode 100644 app/src/main/res/layout/user_info_details_table_item.xml create mode 100644 app/src/main/res/layout/user_info_details_table_item_shimmer.xml create mode 100644 app/src/main/res/layout/view_message_input.xml create mode 100644 app/src/main/res/menu/chat_call_menu.xml create mode 100644 app/src/main/res/menu/chat_send_menu.xml create mode 100644 app/src/main/res/menu/menu_conversation.xml create mode 100644 app/src/main/res/menu/menu_conversation_info.xml create mode 100644 app/src/main/res/menu/menu_conversation_info_edit.xml create mode 100644 app/src/main/res/menu/menu_conversation_plus_filter.xml create mode 100644 app/src/main/res/menu/menu_geocoding.xml create mode 100644 app/src/main/res/menu/menu_locationpicker.xml create mode 100644 app/src/main/res/menu/menu_preview.xml create mode 100644 app/src/main/res/menu/menu_profile.xml create mode 100644 app/src/main/res/menu/menu_search.xml create mode 100644 app/src/main/res/menu/menu_share_files.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/raw/librem_by_feandesign_call.ogg create mode 100644 app/src/main/res/raw/librem_by_feandesign_message.ogg create mode 100644 app/src/main/res/raw/next_voice_message_doodle.ogg create mode 100644 app/src/main/res/raw/tr110_1_kap8_3_freiton1.ogg create mode 100644 app/src/main/res/values-ar/strings.xml create mode 100644 app/src/main/res/values-ast/strings.xml create mode 100644 app/src/main/res/values-b+en+001/strings.xml create mode 100644 app/src/main/res/values-be/strings.xml create mode 100644 app/src/main/res/values-bg-rBG/strings.xml create mode 100644 app/src/main/res/values-ca/strings.xml create mode 100644 app/src/main/res/values-cs-rCZ/strings.xml create mode 100644 app/src/main/res/values-da/strings.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-el/strings.xml create mode 100644 app/src/main/res/values-es-rEC/strings.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-et-rEE/strings.xml create mode 100644 app/src/main/res/values-eu/strings.xml create mode 100644 app/src/main/res/values-fa/strings.xml create mode 100644 app/src/main/res/values-fi-rFI/strings.xml create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values-ga/strings.xml create mode 100644 app/src/main/res/values-gl/strings.xml create mode 100644 app/src/main/res/values-hr/strings.xml create mode 100644 app/src/main/res/values-hu-rHU/strings.xml create mode 100644 app/src/main/res/values-it/strings.xml create mode 100644 app/src/main/res/values-ja-rJP/strings.xml create mode 100644 app/src/main/res/values-ko/strings.xml create mode 100644 app/src/main/res/values-land/dimens.xml create mode 100644 app/src/main/res/values-lt-rLT/strings.xml create mode 100644 app/src/main/res/values-nb-rNO/strings.xml create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100644 app/src/main/res/values-pl/strings.xml create mode 100644 app/src/main/res/values-pt-rBR/strings.xml create mode 100644 app/src/main/res/values-ru/strings.xml create mode 100644 app/src/main/res/values-sc/strings.xml create mode 100644 app/src/main/res/values-sk-rSK/strings.xml create mode 100644 app/src/main/res/values-sl/strings.xml create mode 100644 app/src/main/res/values-sr/strings.xml create mode 100644 app/src/main/res/values-sv/strings.xml create mode 100644 app/src/main/res/values-sw/strings.xml create mode 100644 app/src/main/res/values-sw600dp/dimens.xml create mode 100644 app/src/main/res/values-tr/strings.xml create mode 100644 app/src/main/res/values-ug/strings.xml create mode 100644 app/src/main/res/values-uk/strings.xml create mode 100644 app/src/main/res/values-v27/styles.xml create mode 100644 app/src/main/res/values-v28/arrays.xml create mode 100644 app/src/main/res/values-v28/defaults.xml create mode 100644 app/src/main/res/values-w820dp/dimens.xml create mode 100644 app/src/main/res/values-zh-rCN/strings.xml create mode 100644 app/src/main/res/values-zh-rHK/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/bool.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/defaults.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/setup.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/auth.xml create mode 100644 app/src/main/res/xml/backup_config.xml create mode 100644 app/src/main/res/xml/chip_others.xml create mode 100644 app/src/main/res/xml/chip_you.xml create mode 100644 app/src/main/res/xml/contacts.xml create mode 100644 app/src/main/res/xml/file_provider_paths.xml create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100644 app/src/main/res/xml/syncadapter.xml create mode 100644 app/src/qa/ic_launcher-web.png create mode 100644 app/src/qa/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java create mode 100644 app/src/qa/res/drawable/ic_launcher_background.xml create mode 100644 app/src/qa/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/qa/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/qa/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/qa/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/qa/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/qa/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/qa/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/qa/res/values/setup.xml create mode 100644 app/src/test/java/android/util/Log.kt create mode 100644 app/src/test/java/com/nextcloud/talk/call/CallParticipantListExternalSignalingTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/call/CallParticipantListInternalSignalingTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/call/CallParticipantListTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/call/CallParticipantModelTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/call/LocalCallParticipantModelTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterMcuTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterNoMcuTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/call/MessageSenderTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/contacts/apiService/FakeItem.kt create mode 100644 app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositoryError.kt create mode 100644 app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt create mode 100644 app/src/test/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModelTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/json/ConversationConversionTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/login/data/network/NetworkLoginDataSourceTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverCallParticipantTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverLocalParticipantTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverOfferTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverParticipantListTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverWebRtcTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/test/fakes/FakeCallRecordingRepository.kt create mode 100644 app/src/test/java/com/nextcloud/talk/test/fakes/FakeUnifiedSearchRepository.kt create mode 100644 app/src/test/java/com/nextcloud/talk/utils/BundleKeysTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/utils/DoNotDisturbUtilsTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/utils/UserIdUtilsTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/viewmodels/AbstractViewModelTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/viewmodels/CallRecordingViewModelTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/webrtc/DataChannelMessageNotifierTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/webrtc/GlobalsTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/webrtc/PeerConnectionNotifierTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/webrtc/PeerConnectionWrapperTest.kt create mode 100644 app/src/test/resources/RoomOverallExample_APIv1.json create mode 100644 app/src/test/resources/RoomOverallExample_APIv2.json create mode 100644 app/src/test/resources/RoomOverallExample_APIv4.json create mode 100644 app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 build.gradle create mode 100644 contribute/developer-certificate-of-origin create mode 100644 contribute/developer-certificate-of-origin.license create mode 100644 detekt.yml create mode 100644 docs/TURN.md create mode 100644 docs/branching.png create mode 100644 docs/branching.png.license create mode 100644 docs/branching.svg create mode 100644 docs/branching.svg.license create mode 100644 docs/gplayDebugBuildVariant.png create mode 100644 docs/gplayDebugBuildVariant.png.license create mode 100644 docs/grantNotificationPermissionAfterInstall.png create mode 100644 docs/grantNotificationPermissionAfterInstall.png.license create mode 100644 docs/ignoreBatteryOptimizationDialog.png create mode 100644 docs/ignoreBatteryOptimizationDialog.png.license create mode 100644 docs/ignoreBatteryOptimizationSelectAllApps.png create mode 100644 docs/ignoreBatteryOptimizationSelectAllApps.png.license create mode 100644 docs/ignoreBatteryOptimizationTurnOffSwitch.png create mode 100644 docs/ignoreBatteryOptimizationTurnOffSwitch.png.license create mode 100644 docs/notificationSettingsExample.png create mode 100644 docs/notificationSettingsExample.png.license create mode 100644 docs/notifications.md create mode 100644 docs/notificationsNotSetUpCorrectlyWarning.png create mode 100644 docs/notificationsNotSetUpCorrectlyWarning.png.license create mode 100644 docs/semantic_versioning_code.png create mode 100644 docs/semantic_versioning_code.png.license create mode 100644 drawable_resources/icon-background.svg create mode 100644 drawable_resources/icon-background.svg.license create mode 100644 drawable_resources/icon-foreground.svg create mode 100644 drawable_resources/icon-foreground.svg.license create mode 100644 drawable_resources/icon-foreground_qa.svg create mode 100644 drawable_resources/icon-foreground_qa.svg.license create mode 100644 drawable_resources/other/account-circle.svg create mode 100644 drawable_resources/other/account-circle.svg.license create mode 100644 drawable_resources/other/circular_document.svg create mode 100644 drawable_resources/other/circular_document.svg.license create mode 100644 drawable_resources/other/circular_group.svg create mode 100644 drawable_resources/other/circular_group.svg.license create mode 100644 drawable_resources/other/circular_link.svg create mode 100644 drawable_resources/other/circular_link.svg.license create mode 100644 drawable_resources/other/circular_location.svg create mode 100644 drawable_resources/other/circular_location.svg.license create mode 100644 drawable_resources/other/circular_lock.svg create mode 100644 drawable_resources/other/circular_lock.svg.license create mode 100644 drawable_resources/other/circular_mail.svg create mode 100644 drawable_resources/other/circular_mail.svg.license create mode 100644 drawable_resources/other/file-icon-black-24h.svg create mode 100644 drawable_resources/other/file-icon-black-24h.svg.license create mode 100644 drawable_resources/other/file-icon.svg create mode 100644 drawable_resources/other/file-icon.svg.license create mode 100644 drawable_resources/other/file-password-request.svg create mode 100644 drawable_resources/other/file-password-request.svg.license create mode 100644 drawable_resources/other/ic_crop_16_9.svg create mode 100644 drawable_resources/other/ic_crop_16_9.svg.license create mode 100644 drawable_resources/other/ic_crop_4_3.svg create mode 100644 drawable_resources/other/ic_crop_4_3.svg.license create mode 100644 drawable_resources/other/ic_low_quality.svg create mode 100644 drawable_resources/other/ic_low_quality.svg.license create mode 100644 fastlane/Appfile create mode 100644 fastlane/Appfile.license create mode 100644 fastlane/Fastfile create mode 100644 fastlane/metadata/android/cs-CZ/full_description.txt create mode 100644 fastlane/metadata/android/cs-CZ/short_description.txt create mode 100644 fastlane/metadata/android/cs-CZ/title.txt create mode 100644 fastlane/metadata/android/de-DE/full_description.txt create mode 100644 fastlane/metadata/android/de-DE/short_description.txt create mode 100644 fastlane/metadata/android/de-DE/title.txt create mode 100644 fastlane/metadata/android/el-GR/short_description.txt create mode 100644 fastlane/metadata/android/el-GR/title.txt create mode 100644 fastlane/metadata/android/en-US/full_description.txt create mode 100644 fastlane/metadata/android/en-US/images/featureGraphic.png create mode 100644 fastlane/metadata/android/en-US/images/icon.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/conversationList_dark.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/conversationList_light.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/markdown_light.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/searchParticipant_light.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/serverSelection.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/settings_light.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/voiceCall.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/voiceRecord_light.png create mode 100644 fastlane/metadata/android/en-US/short_description.txt create mode 100644 fastlane/metadata/android/en-US/title.txt create mode 100644 fastlane/metadata/android/es-ES/full_description.txt create mode 100644 fastlane/metadata/android/es-ES/short_description.txt create mode 100644 fastlane/metadata/android/es-ES/title.txt create mode 100644 fastlane/metadata/android/fr-FR/full_description.txt create mode 100644 fastlane/metadata/android/fr-FR/short_description.txt create mode 100644 fastlane/metadata/android/fr-FR/title.txt create mode 100644 fastlane/metadata/android/is-IS/short_description.txt create mode 100644 fastlane/metadata/android/is-IS/title.txt create mode 100644 fastlane/metadata/android/it-IT/full_description.txt create mode 100644 fastlane/metadata/android/it-IT/short_description.txt create mode 100644 fastlane/metadata/android/it-IT/title.txt create mode 100644 fastlane/metadata/android/nl-NL/full_description.txt create mode 100644 fastlane/metadata/android/nl-NL/short_description.txt create mode 100644 fastlane/metadata/android/nl-NL/title.txt create mode 100644 fastlane/metadata/android/pl-PL/short_description.txt create mode 100644 fastlane/metadata/android/pl-PL/title.txt create mode 100644 fastlane/metadata/android/pt-BR/full_description.txt create mode 100644 fastlane/metadata/android/pt-BR/short_description.txt create mode 100644 fastlane/metadata/android/pt-BR/title.txt create mode 100644 fastlane/metadata/android/ru-RU/full_description.txt create mode 100644 fastlane/metadata/android/ru-RU/short_description.txt create mode 100644 fastlane/metadata/android/ru-RU/title.txt create mode 100644 fastlane/metadata/android/sr-SR/full_description.txt create mode 100644 fastlane/metadata/android/sr-SR/short_description.txt create mode 100644 fastlane/metadata/android/sr-SR/title.txt create mode 100644 fastlane/metadata/android/tr-TR/full_description.txt create mode 100644 fastlane/metadata/android/tr-TR/short_description.txt create mode 100644 fastlane/metadata/android/tr-TR/title.txt create mode 100644 fastlane/metadata/android/vi-VI/short_description.txt create mode 100644 fastlane/metadata/android/vi-VI/title.txt create mode 100644 gradle.properties create mode 100644 gradle/verification-keyring.keys create mode 100644 gradle/verification-keyring.keys.license create mode 100644 gradle/verification-metadata.xml create mode 100644 gradle/verification-metadata.xml.license create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.jar.license create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradle/wrapper/gradle-wrapper.properties.license create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 gradlew.bat.license create mode 100644 gradlew.license create mode 100644 renovate.json5 create mode 100644 renovate.json5.license create mode 100755 scripts/analysis/analysis-wrapper.sh create mode 100755 scripts/analysis/getBranchName.sh create mode 100644 scripts/analysis/lint-results.txt create mode 100644 scripts/analysis/lint-results.txt.license create mode 100644 scripts/analysis/lint-up.rb create mode 100755 scripts/analysis/spotbugs-up.rb create mode 100755 scripts/analysis/spotbugsComparison.py create mode 100755 scripts/analysis/spotbugsSummary.py create mode 100755 scripts/checkGplayLimitation.sh create mode 100755 scripts/hooks/pre-commit create mode 100755 scripts/hooks/pre-push create mode 100755 scripts/lib.sh create mode 100644 scripts/metadata/generate_metadata.py create mode 100644 scripts/repo create mode 100644 scripts/repo.license create mode 100644 scripts/tools/OxygenConversion.MD create mode 100755 scripts/wait_for_emulator.sh create mode 100644 settings.gradle create mode 100644 spotbugs-filter.xml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5eb6d7d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,659 @@ + +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +Types of changes can be: Added/Changed/Deprecated/Removed/Fixed/Security + +## [21.1.0] - 2025-06-05 + +### Added +- Allow adding participants to one-to-one chats creating a new conversation +- Handling of event conversations +- Show info about participant (organization, role, timezone, ...) in 1:1 conversation info screen +- Added gallery option in chat attachment menu (access photos and videos with one click without giving permissions) +- Add self-test button for push notifications in diagnosis screen +- Edit checkbox in chat messages +- Team mentions in chat +- Add option to mark a conversation as sensitive +- Allow bluetooth headset to be discovered and used during a call (@gavine99) + +### Changed +- Design of participants grid in call +- Improve subline in conversations screen when last message is attachment +- Improve message search +- In search window, show messages at last +- Switch video capture for calls between 4:3 and 16:9 ratio depending on portrait/ landscape mode + +### Fixed +- Crashes +- Videos in videocall lost after app comes back to foreground +- Open conversations not being shown in search +- Minor bugs (@MmAaXx500) + +Minimum: NC 17 Server, Android 8.0 Oreo + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/94?closed=1 + +## [21.0.1] - 2025-04-15 + +### Fixed +- No sound transmitted from android device in initial call after granting microphone permission +- Duplicated chat messages are shown (e.g. after taking a picture) +- Crashes + +Minimum: NC 17 Server, Android 8.0 Oreo + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/95?closed=1 + +## [21.0.0] - 2025-02-24 + +### Added +- Sending status for text messages +- "Retry" button for messages where sending failed +- Playback speed control for voice messages (@arkascha) +- Auto play consecutive voice messages (@Ruggero1912) +- Show info in offline mode when no chat messages are available for a conversation +- Archive/unarchive conversation from context menu +- Search in main screen shows open conversations and contacts + +### Changed +- Edit & delete buttons for an offline written message (queued state) are moved to the context menu +- Move archive conversation button in conversation-info screen to "Danger zone" + +### Fixed +- Duplicated chat messages are shown after screen rotation +- Chat loading animation overlays with chat messages +- Wrong reaction backgrounds +- Mentioned teams are not rendered in chat +- Crashes +- Minor bugs + +Minimum: NC 17 Server, Android 8.0 Oreo + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/92?closed=1 + +## [20.1.1] - 2025-01-16 + +### Added +- Display conversation avatars for open conversations search + +### Changed +- play ".webm" videos in internal player + +### Fixed +- Video is not shown in android to android calls +- App crashes +- Send and voice-message icons overlay +- minor bugs + +Minimum: NC 17 Server, Android 8.0 Oreo + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/93?closed=1 + +## [20.1.0] - 2024-12-03 + +### Added +- Archive conversations +- Show regular warning if notification settings are not set up correctly (can be disabled) +- Identify guests invited via email address +- Long press options for end call button (leave or end call for all) +- Textsearch in "Join open conversations" screen + +### Changed +- Call started indicator is now at bottom of chat + +Minimum: NC 17 Server, Android 8.0 Oreo + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/86?closed=1 + +## [20.0.6] - 2024-11-28 + +### Added +- Permanent warning if notification settings are not set up correctly (can be disabled) + +Minimum: NC 17 Server, Android 7.0 Nougat + +## [20.0.5] - 2024-11-18 + +### Fixed +- Fix a crash when opening chat + +Minimum: NC 17 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/91?closed=1 + +## [20.0.4] - 2024-11-14 + +### Fixed +- Emojis to react in call are missing (now with horizontal scrolling) +- Fix crashes + +Minimum: NC 17 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/90?closed=1 + +## [20.0.3] - 2024-11-11 + +### Added +- Improvements to offline support ("offline first") +- Queuing and editing of offline written messages +- New "unread messages marker" behavior +- Align grouping of chat messages with server behavior +- Set full screen width for messages in 'Note To Self' (@arkascha) + +### Fixed +- Sometimes offline messages need to be re-downloaded +- Unread messages marker is shown multiple times +- Deck cards are shown as {object} +- Minor bugs + +Minimum: NC 17 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/89?closed=1 + +## [20.0.2] - 2024-09-26 + +### Added +- Improvements for conversation creation + +### Fixed +- Conversation list + chat does not load for very old server versions +- TextInput field is shown when user has no permission to write +- Call buttons sometimes disappear after 30 sec for federated rooms +- Avoid crashes that could happen when entering chat + +Minimum: NC 17 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/88?closed=1 + +## [20.0.1] - 2024-09-17 + +### Fixed +- Chat does not load for older server versions +- Status icons are not shown +- Account is missing in account switcher dialog +- Error for federated invitation is shown although server does not support them + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/87?closed=1 + +## [20.0.0] - 2024-09-14 + +### Added +- Offline support for conversations list and chat +- Federated calls +- Allow banning users and guests +- Open internal links for files app from any screen +- Show conversation description in chat and when listing open conversations + +### Changed +- New workflow for conversation creation + +### Fixed +- Connection fails with wired internet +- Minor bugs + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/85?closed=1 + +## [19.0.1] - 2024-05-23 + +### Fixed +- Wrong availability of "leave conversation" and "delete conversation" +- Chats jump to last message instead to first unread message +- Sent text from "share to" feature is set repeatedly for text input +- Shared files from Nextcloud fail to open Nextcloud files app +- Minor bugs + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/83?closed=1 + +## [19.0.0] - 2024-04-23 + +### Added +- Federated conversations +- Message editing + +### Changed +- Updated file icons + +### Fixed +- Participants in conversation info screen are missing +- Flickering appbar when scrolling conversation list +- Call notification screen is sometimes incomplete/unresponsive +- Polls won't open +- Note to self icon is not shown for languages other than english +- Minor bugs + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/81?closed=1 + +## [18.1.0] - 2024-03-12 + +### Added +- Diagnosis screen (in advanced settings. incl. share option to create new issue) +- Show warnings if notification settings are not set correctly +- Grouping for upload notifications (@parneet-guraya) +- Stop media playback when switching output device (@parneet-guraya) +- Avoid multiple media playbacks (@parneet-guraya) +- Add "Add to notes" in message options +- Cursor position is saved in message drafts +- Share message text to other apps +- Support Android 14 +- Janus 1.x support + +### Fixed +- App permanently sends speaking data channel message +- Back button closes app when forwarding a message + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/79?closed=1 + +## [18.0.1] - 2023-12-22 + +### Fixed +- Voice messages sometimes fail to playback + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/75?closed=1 + +## [18.0.0] - 2023-12-11 + +### Added +- File captions +- Note To Self +- Recording consent +- Share files by long press context menu (@Smarshal21) +- Save files to storage (@FaribaKhandani) +- Show active call in chat with accept call buttons + +### Fixed +- Not possible to delete voice, video, image, contact and location messages (@Smarshal21) +- Hide "unread mention" bubble in search mode (@sowjanyakch) +- Call notification screen remains open +- Minor bug fixes (@parneet-guraya et al.) + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/75?closed=1 + +## [17.1.3] - 2023-11-17 + +### Fixed +- Login via Active Directory fails when using Umlauts in username +- Crash when guest without name joins a call +- Chat messages disappear on initial configuration change (e.g. screen rotation) + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/78?closed=1 + +## [17.1.2] - 2023-10-19 + +### Fixed +- Fix to play voice messages +- Fix to send voice message after recording was stopped to re-listen +- Fix emoji size in markdown headers +- minor bug fixes + +### Changed +- message reminder: TimePicker format matches locale of device +- removed Android Auto support for now + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/76?closed=1 + +## [17.1.0] - 2023-09-15 + +### Added +- Markdown support +- Group system messages +- Set reminders for messages +- List open conversations +- Call duration visible while in a call +- Filter for unread / mentioned conversations +- Continuous recording of voice messages +- Android Auto support +- Keep message drafts +- Show status icon in chatview +- Send indicator that user is speaking when being in a call + +### Fixed +- Media playback does not retain state (@parneet-guraya) +- minor bug fixes + +### Changed +- Adjust app icon size for notifications (@Smarshal21) + +Minimum: Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/71?closed=1 + +## [17.0.2] - 2023-07-24 + +### Fixed +- Fix establishing of call connection to High Performance Backend when rejoining call + +Minimum: NC 14 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/73?closed=1 + +## [17.0.1] - 2023-07-07 + +### Fixed +- Avoid crash when opening conversations (happened when OpenAI translations were enabled) +- Avoid loading conversations screen multiple times after login when multiple accounts are used +- Fix phone book integration +- Minor fixes + +### Changed +- new UI for Settings screen + +Minimum: NC 14 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/72?closed=1 + +## [17.0.0] - 2023-06-12 + +### Added +- Typing indicator (requires NC27 and high performance backend) +- Conversation avatars (requires NC27) +- Reactions in calls (requires NC27) +- Translate chat messages (requires NC27 and translation provider) +- Group mentions in a conversation +- Set conversation description + +### Fixed +- Avatars gone in conversation list (e.g. after screen rotation) + +Minimum: NC 14 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/70?closed=1 + +## [16.0.1] - 2023-04-21 + +### Fixed +- Fix to scroll to first unread message +- Rare crashes + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/69?closed=1 + +## [16.0.0] - 2023-03-20 + +### Added +- Call recording support (requires NC26, HPB + recording server) +- Breakout rooms support (requires NC26 + HPB) +- Raise hand +- Scroll to bottom button in chat (@rapterjet2004) +- Scroll to quoted messages on tap (@rapterjet2004) + +### Fixed +- Display duplicated chatmessages +- Chatmessages not being displayed +- Broken "mention" design when groupname contains emoji +- Avatars/Images not being displayed in some cases +- Fix theming of set status dialog buttons +- Fix call buttons size for landscape mode +- Rare crashes + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/65?closed=1 + +## [15.1.2] - 2023-02-17 + +### Added +- Show raised hands of remote participants + +### Changed +- Better voice message recording quality + +### Fixed +- Missing author in group conversations +- Missing file thumbnails in "share from Nextcloud" +- Repair multiple actions when switching account via notification +- Missing "back" button when opening chat by notification +- Rare crashes + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/67?closed=1 + +## [15.1.1] - 2023-01-18 + +### Fixed + +- Missing file icons in chat +- "Random" crashes +- Call connections + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/66?closed=1 + +## [15.1.0] - 2023-01-12 + +### Added +- Support latest emojis +- Localize time formatting + +### Changed +- Android 6 required +- Improvements to calls + +### Fixed +- Crash on startup because of Unknown color +- The video of the first remote participant is eventually disabled +- Show notifications for missed calls and improve duration for ringing +- Too hard to react with already given reactions +- Crash when joining call when ringtone is set to silent + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/61?closed=1 + +## [15.0.3] - 2022-12-15 + +### Fixed +- App crash on startup when having multiple accounts +- Accounts seem to disappear +- Wrong conversation title / conversation title sometimes switches + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/64?closed=1 + +## [15.0.2] - 2022-12-01 + +### Fixed +- Fail to show user status in conversation list in some scenarios +- Fail to upload files on some devices +- Fail to pick avatar from local gallery +- Minor call issues + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/63?closed=1 + +## [15.0.1] - 2022-10-19 + +### Fixed +- Defect translations for regions like de_AT + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/62?closed=1 + +## [15.0.0] - 2022-10-17 + +### Added +- Simple polls (Talk 15 required on server) +- Direct video upload +- Show upload notification with progress bar for files >1MB +- Server theming +- Handle expiring messages (Talk 15 required on server) +- Respect permissions set by moderators +- Account chooser for "share to Nextcloud" +- Link previews (Talk 15 required on server) + +### Changed +- Update design to Material 3 +- Move allow guests preferences to conversation info + +### Fixed +- Load higher resolution avatars in conversation list +- Upload large files + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/59?closed=1 + +## [14.2.0] - 2022-08-31 +### Added + +- Tabs for deck cards, locations and other objects in shared items view +- "Mark as read" via notification +- Create new profile avatar image with camera + +### Changed + +- Load higher resolution avatars in conversation list + +### Fixed + +- Fail to open newly created conversation +- Starting a call from chat screen crashes +- Rare crashes during swipe left for reply to a message + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/57?closed=1 + +## [14.1.1] - 2022-08-15 +### Fixed +- Swipe left for reply to a message +- Multiple minor issues + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/58?closed=1 + +## [14.1.0] - 2022-07-18 +### Added +- Search within messages +- Quick reply via notification (@starypatyk) +### Changed +- Scroll to oldest unread message when opening a conversation +### Fixed +- No conversations loaded when user status app is limited to groups (server setting) +- Minor bugfixes + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/52?closed=1 + +## [14.0.2] - 2022-05-13 +### Added +- Handling for "event.participants.update.all" from HPB +### Fixed +- Multiple NPE +- Reactions option for deleted messages and commands are shown +- Always show reaction count (not only > 1) +- Reactions option shown in read-only conversations + +For a full list, please see https: https://github.com/nextcloud/talk-android/milestone/55?closed=1 + +## [14.0.1] - 2022-05-03 +- fix app crashes when UI isn't available anymore +- fix to load conversations when servers status app is disabled + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/54?closed=1 + +## [14.0.0] - 2022-05-02 +### Added +- emoji message reactions +- set own user status / show user status of others +- show shared items of a conversation +- search for open conversations +- mark conversation as read +- select audio output for calls +- choose notification sounds by android settings (starypatyk) +- share contact from attachment dialog + +### Fixed +- call connection from android to web sometimes fail on HPB +- top bar partially hidden when typing message +- can't open chat view from notification (starypatyk) +- minor fixes + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/50?closed=1 + +## [13.0.0] - 2021-11-29 +### Added +- set own user status / show user status of others +- search for open conversations +- mark conversation as read (@AndyScherzinger) +- select audio output for calls +- choose notification sounds by android settings (@starypatyk) +- share contact from attachment dialog + +### Fixed +- top bar remains fully visible when typing message +- minor fixes + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/50?closed=1 + +## [12.2.1] - 2021-09-02 +- clear chat history (as moderator) +- forward text messages +- RTL support + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/45?closed=1 + +## [12.1.2] - 2021-07-16 +- fix to share link from chrome +- make links clickable in conversation description +- minor fixes + +For a full list, please see https://github.com/nextcloud/talk-android/milestone/47?closed=1 + +## [12.1.1] - 2021-07-09 +### Fixed +- fix crash on startup (happened for some older Nextcloud server versions) +- fix to receive notifications when using Nextcloud server 22 +- fix background of send button (when server version is <22) +- minor fixes + +## [12.1.0] - 2021-07-06 +### Added +- "share to Nextcloud Talk" from other apps +- location sharing (requires Talk 12 on server) +- voice messages (requires Talk 12 on server) +- open files inside app (jpg, .png, .gif, .mp3, .mp4, .mov, .wav, .txt, .md) + - other data types are opened with external apps if they are able to handle it +- edit profile information and privacy settings +- add grid view for calls, make own video movable +- improve vcard support + +### Changed +- improve conversation list design and dark/light theming (@AndyScherzinger) +- introduce new dark/light toolbar/searchbar design (@AndyScherzinger) +- improve login screen design (@AndyScherzinger) +- improve content/toolbar alignments (@AndyScherzinger) +- various design improvements (@AndyScherzinger) + +### Fixed +- @ in username is allowed for phonebook sync +- avoid sync when phonebook is empty +- avoid creation of multiple "chat via"-links in phonebook +- delete "chat via"-link from phonebook if phone number was deleted on server +- remove all "chat via"-links from phonebook when sync is disabled +- fix to show avatars for incoming pictures in group chats (@starypatyk) +- do not allow selecting files in files browser that are not allowed to be reshared +- fix to show all file previews +- don't keep screen enabled in chat view +- fix logfile flooding (Too much storage was used when the app was offline and a high performance backend is used) + +## [11.1.0] - 2021-03-12 +### Added +- add ability to enter own phone number when address book sync is enabled + +### Fixed +- show links for deck-cards + +## [11.0.0] - 2021-02-23 +### Added +- upload files from local storage +- delete messages (requires Talk 11.1 on server) +- UI-improvements for call screens +- new ringtone for outgoing calls diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..49e67aa --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ + +In the Nextcloud community, participants from all over the world come together to create Free Software for a free internet. This is made possible by the support, hard work and enthusiasm of thousands of people, including those who create and use Nextcloud software. + +Our code of conduct offers some guidance to ensure Nextcloud participants can cooperate effectively in a positive and inspiring atmosphere, and to explain how together we can strengthen and support each other. + +The Code of Conduct is shared by all contributors and users who engage with the Nextcloud team and its community services. It presents a summary of the shared values and "common sense" thinking in our community. + +You can find our full code of conduct on our website: https://nextcloud.com/code-of-conduct/ + +Please, keep our CoC in mind when you contribute! That way, everyone can be a part of our community in a productive, positive, creative and fun way. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a778b58 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,412 @@ + +# [Nextcloud](https://nextcloud.com) Talk for Android app + +# Index +1. [Guidelines](#guidelines) + 1. [Issue reporting](#issue-reporting) + 1. [Labels](#labels) + 1. [Pull request](#pull-request) + 1. [Issue](#issue) + 1. [Bug workflow](#bug-workflow) +1. [Contributing to Source Code](#contributing-to-source-code) + 1. [Developing process](#developing-process) + 1. [Branching model](#branching-model) + 1. [Android Studio formatter setup](#android-studio-formatter-setup) + 1. [Build variants](#build-variants) + 1. [Contribution process](#contribution-process) + 1. [Fork and download android repository](#fork-and-download-android-repository) + 1. [Create pull request](#create-pull-request) + 1. [Create another pull request](#create-another-pull-request) + 1. [Backport pull request](#backport-pull-request) + 1. [Adding new files](#adding-new-files) + 1. [Testing](#testing) + 1. [File naming](#file-naming) + 1. [Menu files](#menu-files) + 1. [Translations](#translations) + 1. [Engineering practices](#engineering-practices) + 1. [Approach to technical debt](#approach-to-technical-debt) + 1. [Dependency injection](#dependency-injection) + 1. [Testing](#testing) +1. [Releases](#releases) + 1. [Types](#types) + 1. [Stable](#stable) + 1. [Release Candidate](#release-candidate) + 1. [Alpha Release](#alpha-release) + 1. [QA Release](#qa-release) + 1. [Version Name and number](#version-name-and-number) + 1. [Stable / Release candidate](#stable--release-candidate) + 1. [Release cycle](#release-cycle) + 1. [Release Process](#release-process) + 1. [Stable Release](#stable-release) + 1. [Release Candidate Release](#release-candidate-release) + 1. [Alpha Release](#alpha-release) + +# Guidelines + +## Issue reporting + +* [Report the issue](https://github.com/nextcloud/talk-android/issues/new/choose) and choose bug report or feature request. The template includes all the information we need to track down the issue. +* This repository is *only* for issues within the Nextcloud Talk Android app code. Issues in other components should be reported in their own repositories, e.g. [Nextcloud server](https://github.com/nextcloud/server/issues) +* Search the [existing issues](https://github.com/nextcloud/talk-android/issues) first, it's likely that your issue was already reported. + +If your issue appears to be a bug, and hasn't been reported, open a new issue. + +## Labels + +### Pull request + +* 2 developing +* 3 to review + +### Issue + +* nothing +* approved +* PR exists (and then the PR# should be shown in first post) + +### Bug workflow + +Every bug should be triaged in approved/needs info in a given time. +* approved: at least one other is able to reproduce it +* needs info: something unclear, or not able to reproduce + * if no response within 1 months, bug will be closed +* pr exists: if bug is fixed, link to pr + +# Contributing to Source Code + +Thanks for wanting to contribute source code to Nextcloud. That's great! + +New contributions are added under GPL version 3+. + +## Developing process + +We are all about quality while not sacrificing speed so we use a very pragmatic workflow. + +* create an issue with feature request + * discuss it with other developers + * create mockup if necessary + * must be approved --> label approved + * after that no conceptual changes! +* develop code +* create [pull request](https://github.com/nextcloud/talk-android/pulls) +* to assure the quality of the app, any PR gets reviewed, approved and tested before it will be merged to master + +### Branching model + +![branching model](/docs/branching.png "Branching Model") +* All contributions (bug fix or feature PRs) target the ```master``` branch +* Feature releases will always be based on ```master``` +* Bug fix releases will always be based on their respective feature-release-bug-fix-branches +* Bug fixes relevant for the most recent _and_ released feature (e.g. ```11.0.0```) or bugfix (e.g. ```11.2.1```) release will be backported to the respective bugfix branch (e.g. ```stable-11.0``` or ```stable-11.2```) +* Hot fixes not relevant for an upcoming feature release but the latest release can target the bug fix branch directly + +### Android Studio formatter setup + +Our formatter setup is rather simple: +* Standard Android Studio +* Line length 120 characters (```Settings``` → ```Editor``` → ```Code Style``` → ```Right margin(columns)```: 120) +* Auto optimize imports (```Settings``` → ```Editor``` → ```Auto Import``` → ```Optimize imports on the fly```) + +### Build variants + +There are three build variants +* generic: no Google Stuff, used for F-Droid +* gplay: with Google Stuff (Push notification), used for Google Play Store +* qa: based on pr and available as direct download within the pr for testing purposes + +### Apply a license + +Nextcloud doesn't require a CLA (Contributor License Agreement). +The copyright belongs to all the individual contributors. +Therefore we recommend that every contributor adds following line to the header of a file, if they changed it substantially: + +``` +Copyright (c) +``` + +See section [Adding new files](#adding-new-files) for templates which can be used in new files. + +### Sign your work + +We use the Developer Certificate of Origin (DCO) as a additional safeguard for the Nextcloud project. +This is a well established and widely used mechanism to assure contributors have confirmed their right to license their contribution under the project's license. +Please read [developer-certificate-of-origin][dcofile]. +If you can certify it, then just add a line to every git commit message: + +```` + Signed-off-by: Random J Developer +```` + +Use your real name (sorry, no pseudonyms or anonymous contributions). +If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`. +You can also use git [aliases](https://git-scm.com/book/tr/v2/Git-Basics-Git-Aliases) like `git config --global alias.ci 'commit -s'`. +Now you can commit with `git ci` and the commit will be signed. + +### Git hooks + +We provide git hooks to make development process easier for both the developer and the reviewers. +To install them, just run: + +```bash +./gradlew installGitHooks +``` + +## Contribution process + +Contribute your code targeting/based-on the branch ```master```. +It will give us a better chance to test your code before merging it with stable code. + +### Fork and download android repository: + +* Please follow [SETUP.md](/SETUP.md) to setup Nextcloud Talk Android app work environment. + +### Create pull request: + +* Commit your changes locally: ```git commit -a``` +* Push your changes to your GitHub repo: ```git push``` +* Browse to and issue pull request +* Enter description and send pull request. + +### Create another pull request: + +To make sure your new pull request does not contain commits which are already contained in previous PRs, create a new branch which is a clone of upstream/master. + +* ```git fetch upstream``` +* ```git checkout -b my_new_master_branch upstream/master``` +* If you want to rename that branch later: ```git checkout -b my_new_master_branch_with_new_name``` +* Push branch to server: ```git push -u origin name_of_local_master_branch``` +* Use GitHub to issue PR + +### Backport pull request: + +Use backport-bot via "/backport to stable-version", e.g. "/backport to stable-11.2". +This will automatically add "backport-request" label to PR and bot will create a new PR to targeted branch once the base PR is merged. +If automatic backport fails, it will create a comment. + +### Adding new files + +If you create a new file it needs to contain a license header. We encourage you to use the same license (GPL3+) as we do. +Copyright of Nextcloud GmbH is optional. + +Source code of app: +```java/kotlin +/* + * Nextcloud Talk application + * + * @author Your Name + * Copyright (C) 2021 Your Name + * Copyright (C) 2021 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + ``` + + XML (layout) file: + ```xml + +``` + +## File naming + +The file naming patterns are inspired and based on [Ribot's Android Project And Code Guidelines](https://github.com/ribot/android-guidelines/blob/c1d8c9c904eb31bf01fe24aadb963b74281fe79a/project_and_code_guidelines.md). + +### Menu files + +Similar to layout files, menu files should match the name of the component. For example, if we are defining a menu file that is going to be used in the `UserProfileActivity`, then the name of the file should be `activity_user_profile.xml`. Same pattern applies for menus used in adapter view items, dialogs, etc. + +| Component | Class Name | Menu Name | +| ---------------- | ---------------------- | ----------------------------- | +| Activity | `UserProfileActivity` | `activity_user_profile.xml` | +| Fragment | `SignUpFragment` | `fragment_sign_up.xml` | +| Dialog | `ChangePasswordDialog` | `dialog_change_password.xml` | +| AdapterView item | --- | `item_person.xml` | +| Partial layout | --- | `partial_stats_bar.xml` | + +A good practice is to not include the word `menu` as part of the name because these files are already located in the `menu` directory. In case a component uses several menus in different places (via popup menus) then the resource name would be extended. For example, if the user profile activity has two popup menus for configuring the users settings and one for the handling group assignments then the file names for the menus would be: `activity_user_profile_user_settings.xml` and `activity_user_profile_group_assignments.xml`. + +## Translations + +We manage translations via [Transifex](https://app.transifex.com/nextcloud/nextcloud/talk-android/). So just request +joining the translation team for Android on the site and start translating. All translations will then be automatically pushed to this repository, there is no need for any pull request for translations. + +When submitting PRs with changed translations, please only submit changes to values/strings.xml and not changes to translated files. These will be overwritten by the next merge of transifex-but and would increase PR review efforts. + +## Engineering practices + +This section contains some general guidelines for new contributors, based on common issues flagged during code review. + +### Approach to technical debt + +TL;DR Non-Stop Litter Picking Party! + +We recognize the importance of technical debt that can slow down development, make bug fixing difficult and +discourage future contributors. + +We are mindful of the [Broken Windows Theory](https://en.wikipedia.org/wiki/Broken_windows_theory) and we'd like to +actively promote and encourage contributors to apply The Scout's Rule: *"Always leave the campground cleaner than +you found it"*. Simple, little improvements will sum up and will be very appreciated by Nextcloud team. + +We also promise to actively support and mentor contributors that help us to improve code quality, as we understand +that this process is challenging and requires deep understanding of the application codebase. + +### Dependency injection + +TL;DR Avoid calling constructors inside constructors. + +In effort to modernize the codebase we are applying [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) +whenever possible. We use 2 approaches: automatic and manual. + +We are using [Dagger 2](https://dagger.dev/) to inject dependencies into major Android components only: + + * `Activity` + * `Fragment` + * `Service` + * `BroadcastReceiver` + * `ContentProvider` + +This process is fairly automatic, with `@Inject` annotation being sufficient to supply properly initialized +objects. Android lifecycle callbacks allow us to do most of the work without effort. + +For other application sub-components we prefer to use constructor injection and manually provide required dependencies. + +This combination allows us to benefit from automation when it provides most value, does not tie the rest of the code +to any specific framework and stimulates continuous code modernization through iterative refactoring of all minor +elements. + +### Testing + +TL;DR If we can't write a test for it, it's not good. + +Test automation is challenging in mobile applications in general. We try to improve in this area +and thereof we'd ask contributors to be mindful of their code testability: + +1. new code submitted to Nextcloud project should be provided with automatic tests +2. contributions to existing code that is currently not covered by automatic tests + should at least not make future efforts more challenging +3. whenever possible, testability should be improved even if the code is not covered by tests + +# Releases + +At the moment we are releasing the app in two app stores: + +* [Google Play Store](https://play.google.com/store/apps/details?id=com.nextcloud.talk2) +* [F-Droid](https://f-droid.org/en/packages/com.nextcloud.talk2/) + +## Types + +We do differentiate between three different kinds of releases: + +### Stable + +Play store and f-droid releases for the masses. +Pull Requests that have been tested and reviewed can go to master. After the last alpha release is out in the wild and no mayor errors get reported (by users or in the developer console) the master branch is ready for the stable release phase. +So when we decide to go for a new release we freeze the master feature wise and a stable branch will be created. + +### Release Candidate + +_stable beta_ releases done via the Beta program of the Google Play store. +Whenever a PR is reviewed/approved we put it on master. +Before releasing a new stable version there is at least one release candidate. It is based on the current stable-branch. After a beta testing phase a stable version will be released, which is identical to the latest release candidate. + +### Alpha Release + +_alpha_ releases done via the Alpha program of the Google Play store. +Whenever a PR is reviewed/approved we put it on master. +Alpha releases are based on latest master and and we aim to release a new alpha version on a weekly basis. + +### QA Release + +Done as a standalone app that can be installed in parallel to the stable app. +Any PR gets a QA build so users and reporters are able to easily test the change (feature or bugfix). + +## Version Name and number + +### Stable / Release candidate + +For _stable_ and _release candidate_ the version name follows the [semantic versioning schema](http://semver.org/) and the version number has several digits reserved to parts of the versioning schema inspired by the [jayway version numbering](https://www.jayway.com/2015/03/11/automatic-versioncode-generation-in-android-gradle/), where: + +* 2 digits for beta/alpha code as in release candidates starting at '01' (1-50=Alpha / 51-89=RC / 90-99=stable) +* 2 digits for hot fix code +* 3 digits for minor version code +* n digits for mayor version code + +![Version code schema](/docs/semantic_versioning_code.png "Semantic versioning code") + +Examples for different versions: +version name|version code +---|--- +1.0.0|```10000099``` +8.12.2|```80120290``` +9.8.4-Alpha18|```90080418``` +11.2.0-rc1|```110020051``` + +Beware that beta releases for an upcoming version will always use the minor and hotfix version of the release they are targeting. So to make sure the version code of the upcoming stable release will always be higher stable releases set the 2 beta digits to '90'-'99' as seen above in the examples. For major versions, as we're not a library and thus 'incompatible API changes' is not something that happens, decisions are essentially marketing-based. If we deem a release to be very impactful, we might increase the major version number. + +## Release cycle + +* major releases are linked to the corresponding [server-releases](https://apps.nextcloud.com/apps/spreed/releases) with aligned release date and version number (server version = 11 = client version) +* feature releases are planned every ~2 months, with 6 weeks of developing and 2 weeks of stabilising +* after feature freeze a public release candidate on play store and f-droid is released +* ~2 weeks testing, bug fixing +* release final version on f-droid and play store +* bugfix releases (dot releases, e.g. 3.2.1) are released 4 weeks after stable version from the branch created with first stable release (stable-3.2). + +Hotfixes as well as security fixes are released via bugfix releases (dot releases) but are released on demand in contrast to regular, scheduled bugfix releases. + +To get an idea which PRs and issues will be part of the next release simply check our [milestone plan](https://github.com/nextcloud/talk-android/milestones) + +## Release process + +### Stable Release + +Stable releases are based on the git [stable-*](https://github.com/nextcloud/talk-android/branches/all?query=stable-). + +1. Bump the version name and version code in the [/app/build.gradle](https://github.com/nextcloud/talk-android/blob/master/app/build.gradle), see chapter 'Version Name and number'. +2. Create a [release/tag](https://github.com/nextcloud/talk-android/releases) in git. Tag name following the naming schema: ```stable-Mayor.Minor.Hotfix``` (e.g. stable-1.2.0) naming the version number following the [semantic versioning schema](http://semver.org/) + +### Release Candidate Release + +Release Candidate releases are based on the git [stable-*](https://github.com/nextcloud/talk-android/branches/all?query=stable-) and are before publishing stable releases. + +1. Bump the version name and version code in the [/app/build.gradle](https://github.com/nextcloud/talk-android/blob/master/app/build.gradle), see below the version name and code concept. +2. Create a [release/tag](https://github.com/nextcloud/talk-android/releases) in git. Tag name following the naming schema: ```rc-Mayor.Minor.Hotfix-betaIncrement``` (e.g. rc-1.2.0-12) naming the version number following the [semantic versioning schema](http://semver.org/) + +### Alpha Release + +Release Candidate releases are based on the git [master](https://github.com/nextcloud/talk-android) and are done between stable releases. + +1. Bump the version name and version code in the [/app/build.gradle](https://github.com/nextcloud/talk-android/blob/master/app/build.gradle), see below the version name and code concept. +2. Create a [release/tag](https://github.com/nextcloud/talk-android/releases) in git. Tag name following the naming schema: ```rc-Mayor.Minor.Hotfix-betaIncrement``` (e.g. rc-1.2.0-12) naming the version number following the [semantic versioning schema](http://semver.org/) diff --git a/LICENCE.md b/LICENCE.md new file mode 100644 index 0000000..5637551 --- /dev/null +++ b/LICENCE.md @@ -0,0 +1,679 @@ + + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/LICENSES/AGPL-3.0-or-later.txt b/LICENSES/AGPL-3.0-or-later.txt new file mode 100644 index 0000000..0c97efd --- /dev/null +++ b/LICENSES/AGPL-3.0-or-later.txt @@ -0,0 +1,235 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt new file mode 100644 index 0000000..13ca539 --- /dev/null +++ b/LICENSES/CC-BY-4.0.txt @@ -0,0 +1,156 @@ +Creative Commons Attribution 4.0 International + + Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. + +Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +Section 1 – Definitions. + + a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + + d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + + g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. + + i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + + 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + + 5. Downstream recipients. + + A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + + 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. Other rights. + + 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this Public License. + + 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; + + b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + + a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. + + b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. + + c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + + a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + + c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + + d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + + e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +Section 8 – Interpretation. + + a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + + c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + + d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSES/CC-BY-SA-3.0.txt b/LICENSES/CC-BY-SA-3.0.txt new file mode 100644 index 0000000..39a8591 --- /dev/null +++ b/LICENSES/CC-BY-SA-3.0.txt @@ -0,0 +1,99 @@ +Creative Commons Attribution-ShareAlike 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. + + b. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined below) for the purposes of this License. + + c. "Creative Commons Compatible License" means a license that is listed at http://creativecommons.org/compatiblelicenses that has been approved by Creative Commons as being essentially equivalent to this License, including, at a minimum, because that license: (i) contains terms that have the same purpose, meaning and effect as the License Elements of this License; and, (ii) explicitly permits the relicensing of adaptations of works made available under that license under this License or a Creative Commons jurisdiction license with the same License Elements as this License. + + d. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership. + + e. "License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike. + + f. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. + + g. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. + + h. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. + + i. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. + + j. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. + + k. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; + + b. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified."; + + c. to Distribute and Publicly Perform the Work including as incorporated in Collections; and, + + d. to Distribute and Publicly Perform Adaptations. + + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; + + ii. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, + + iii. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(c), as requested. + + b. You may Distribute or Publicly Perform an Adaptation only under the terms of: (i) this License; (ii) a later version of this License with the same License Elements as this License; (iii) a Creative Commons jurisdiction license (either this or a later license version) that contains the same License Elements as this License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible License. If you license the Adaptation under one of the licenses mentioned in (iv), you must comply with the terms of that license. If you license the Adaptation under the terms of any of the licenses mentioned in (i), (ii) or (iii) (the "Applicable License"), you must comply with the terms of the Applicable License generally and the following provisions: (I) You must include a copy of, or the URI for, the Applicable License with every copy of each Adaptation You Distribute or Publicly Perform; (II) You may not offer or impose any terms on the Adaptation that restrict the terms of the Applicable License or the ability of the recipient of the Adaptation to exercise the rights granted to that recipient under the terms of the Applicable License; (III) You must keep intact all notices that refer to the Applicable License and to the disclaimer of warranties with every copy of the Work as included in the Adaptation You Distribute or Publicly Perform; (IV) when You Distribute or Publicly Perform the Adaptation, You may not impose any effective technological measures on the Adaptation that restrict the ability of a recipient of the Adaptation from You to exercise the rights granted to that recipient under the terms of the Applicable License. This Section 4(b) applies to the Adaptation as incorporated in a Collection, but this does not require the Collection apart from the Adaptation itself to be made subject to the terms of the Applicable License. + + c. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Ssection 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. + + d. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. + + b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. + + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. + + c. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + + d. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. + + e. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. + + f. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. + +Creative Commons Notice + +Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. + +Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of the License. + +Creative Commons may be contacted at http://creativecommons.org/. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt new file mode 100644 index 0000000..17cb286 --- /dev/null +++ b/LICENSES/GPL-2.0-only.txt @@ -0,0 +1,117 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. + + c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author + + This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/LICENSES/GPL-3.0-or-later.txt b/LICENSES/GPL-3.0-or-later.txt new file mode 100644 index 0000000..f6cdd22 --- /dev/null +++ b/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/LICENSES/LGPL-2.1-or-later.txt b/LICENSES/LGPL-2.1-or-later.txt new file mode 100644 index 0000000..c9aa530 --- /dev/null +++ b/LICENSES/LGPL-2.1-or-later.txt @@ -0,0 +1,175 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: + + a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. + + e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. + + b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the library's name and an idea of what it does. + Copyright (C) year name of author + + This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in +the library `Frob' (a library for tweaking knobs) written +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 +Ty Coon, President of Vice +That's all there is to it! diff --git a/LICENSES/LGPL-3.0-or-later.txt b/LICENSES/LGPL-3.0-or-later.txt new file mode 100644 index 0000000..513d1c0 --- /dev/null +++ b/LICENSES/LGPL-3.0-or-later.txt @@ -0,0 +1,304 @@ +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. + +"The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + + a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. + +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license document. + +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + + a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license document. + + c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. + + e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) + +5. Combined Libraries. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. + +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/LICENSES/LicenseRef-NextcloudTrademarks.txt b/LICENSES/LicenseRef-NextcloudTrademarks.txt new file mode 100644 index 0000000..464a30b --- /dev/null +++ b/LICENSES/LicenseRef-NextcloudTrademarks.txt @@ -0,0 +1,9 @@ +The Nextcloud marks +Nextcloud and the Nextcloud logo is a registered trademark of Nextcloud GmbH in Germany and/or other countries. +These guidelines cover the following marks pertaining both to the product names and the logo: “Nextcloud” +and the blue/white cloud logo with or without the word Nextcloud; the service “Nextcloud Enterprise”; +and our products: “Nextcloud Files”; “Nextcloud Groupware” and “Nextcloud Talk”. +This set of marks is collectively referred to as the “Nextcloud marks.” + +Use of Nextcloud logos and other marks is only permitted under the guidelines provided by the Nextcloud GmbH. +A copy can be found at https://nextcloud.com/trademarks/ diff --git a/LICENSES/LicenseRef-XTrademarks.txt b/LICENSES/LicenseRef-XTrademarks.txt new file mode 100644 index 0000000..46b6983 --- /dev/null +++ b/LICENSES/LicenseRef-XTrademarks.txt @@ -0,0 +1,49 @@ +Trademark policy +April 2023 + + +You may not violate others’ intellectual property rights, including copyright and trademark. + +A trademark is a word, logo, phrase, or device that distinguishes a trademark holder’s good or service in the marketplace. Trademark law may prevent others from using a trademark in an unauthorized or confusing manner. + + +What is in violation of this policy? + +Using another’s trademark in a way that may mislead or confuse people about your affiliation may be a violation of our trademark policy. + + +What is not a violation of this policy? + +Referencing another’s trademark is not automatically a violation of X's trademark policy. Examples of non-violations include: + +* using a trademark in a way that is outside the scope of the trademark registration e.g., in a different territory, or a different class of goods or services than that identified in the registration; and +* using a trademark in a nominative or other fair use manner. For more information, see our Misleading and deceptive identities policy (https://help.twitter.com/en/rules-and-policies/twitter-impersonation-and-deceptive-identities-policy.html). + + +Who can report violations of this policy? + +X only investigates requests that are submitted by the trademark holder or their authorized representative e.g., a legal representative or other representative for a brand. + + +How can I report violations of this policy? + +You can submit a trademark report through our trademark report form (https://help.twitter.com/forms/trademark). Please provide all the information requested in the form. If you submit an incomplete report, we’ll need to follow up about the missing information. Please note that this will result in a delay in processing your report. + +Note: We may provide the account holder with your name and other information included in the copy of the report. + + +What happens if you violate this policy? + +If we determine that you violated our trademark policy, we may suspend your account. Depending on the type of violation, we may give you an opportunity to comply with our policies. In other instances, an account may be permanently suspended upon first review. If you believe that your account was suspended in error, you can submit an appeal (https://help.twitter.com/forms/general?subtopic=suspended). + + +Additional resources + +Learn more about our range of enforcement options (https://help.twitter.com/rules-and-policies/enforcement-options) and our approach to policy development and enforcement (https://help.twitter.com/rules-and-policies/enforcement-philosophy). + + +Legal disclaimer + +By using the X trademarks and resources on this site, you agree to follow the X Trademark Guidelines in our Brand Guidelines — as well as our Terms of Service and all other X rules and policies. If you have any questions, contact us at trademarks@x.com. + +A copy can be found at https://about.x.com/en/who-we-are/brand-toolkit and https://help.twitter.com/en/rules-and-policies/x-trademark-policy diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..2071b23 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8bd80b0 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ + +# [Nextcloud](https://nextcloud.com) Talk for Android :speech_balloon: + +[![Build Status](https://drone.nextcloud.com/api/badges/nextcloud/talk-android/status.svg)](https://drone.nextcloud.com/nextcloud/talk-android) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/b89a720efbd24754984a776804913bca)](https://www.codacy.com/gh/nextcloud/talk-android/dashboard) [![Releases](https://img.shields.io/github/release/nextcloud/talk-android.svg)](https://github.com/nextcloud/talk-android/releases/latest) [![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/talk-android)](https://api.reuse.software/info/github.com/nextcloud/talk-android) + +[Download from Google Play](https://play.google.com/store/apps/details?id=com.nextcloud.talk2) +[Get it on F-Droid](https://f-droid.org/packages/com.nextcloud.talk2/) + +Please note that Notifications won't work with the F-Droid version due to missing Google Play Services. + +||||||| +|---|---|---|---|---|---| +|![Conversation list](/fastlane/metadata/android/en-US/images/phoneScreenshots/conversationList_light.png "Conversation list")|![Participant search](/fastlane/metadata/android/en-US/images/phoneScreenshots/searchParticipant_light.png "Participant search")|![Voice call](/fastlane/metadata/android/en-US/images/phoneScreenshots/voiceCall.png "Voice call")|![Voice recording](/fastlane/metadata/android/en-US/images/phoneScreenshots/voiceRecord_light.png "Voice recording")|![Markdown view](/fastlane/metadata/android/en-US/images/phoneScreenshots/markdown_light.png "Markdown view")|![Settings](/fastlane/metadata/android/en-US/images/phoneScreenshots/settings_light.png "Settings")| + +**Video & audio calls through Nextcloud on Android** + +Nextcloud Talk is a fully on-premises audio/video and chat communication service. It features web and mobile apps and is designed to offer the highest degree of security while being easy to use. + +Nextcloud Talk lowers the barrier for communication and lets your team connect any time, any where, on any device, with each other, customers or partners. + +## Why is this so awesome? :sparkles: + +Because it is self hosted!!! Audio/video calls and text chat typically require a central server. Some projects go commendably far in trying to ensure they can't see the data, so nobody, not government, advertising company or somebody who broke in the servers, can follow conversations. But the servers still have to mediate every call and text message, allowing them to map out who talks to who and at what time. This 'metadata' [is as useful](https://www.wired.com/2015/03/data-and-goliath-nsa-metadata-spying-your-secrets/), if not more, to track people, than the full content, especially for mass surveillance purposes. Even if the data is not stored by the chat server, the hosting provider or a hacker could simply gather the data. + +By hosting your own server, all meta data stays on your server and thus under your control! + +If you have suggestions or problems, please [open an issue](https://github.com/nextcloud/talk-android/issues) or contribute directly :) + +## How to contribute :rocket: + +If you want to [contribute](https://nextcloud.com/contribute/) to Nextcloud, you are very welcome: + +- on [our public Talk team conversation](https://cloud.nextcloud.com/call/c7fz9qpr) +- our forum at https://help.nextcloud.com +- for translations of the app on [Transifex](https://app.transifex.com/nextcloud/nextcloud/android-talk/) +- opening issues and PRs (including a corresponding issue) + +## Contribution Guidelines :scroll: + +[GPLv3](https://github.com/nextcloud/talk-android/blob/master/LICENSE.txt). All contributions to this repository are considered to be licensed under the GNU GPLv3 or any later version. + +Please read the [Code of Conduct](https://nextcloud.com/community/code-of-conduct/). This document offers some guidance to ensure Nextcloud participants can cooperate effectively in a positive and inspiring atmosphere, and to explain how together we can strengthen and support each other. + +Please review the [guidelines for contributing](/CONTRIBUTING.md) to this repository. + +More information how to contribute: [https://nextcloud.com/contribute/](https://nextcloud.com/contribute/) + +## Start contributing :hammer_and_wrench: + +Make sure you read [SETUP.md](/SETUP.md) and [CONTRIBUTING.md](/CONTRIBUTING.md) before you start working on this project. +But basically: fork this repository and contribute back using pull requests to the master branch. +Easy starting points are also reviewing [pull requests](https://github.com/nextcloud/talk-android/pulls) and working on [starter issues](https://github.com/nextcloud/talk-android/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). + +### Testing :test_tube: + +So you would like to contribute by testing? Awesome, we appreciate that very much. + +To report a bug for the alpha or beta version, just create an issue on github like you would for the stable version and + provide the version number. Please remember that Google Services are necessary to receive push notifications. + +#### Beta versions (Release Candidates) :package: + +##### via Google Play + +Sign up at [Google Play Beta channel](https://play.google.com/apps/testing/com.nextcloud.talk2) to get Release Candidates via Google Play. + +##### via github + +You can also get the Release Candidates at [github releases](https://github.com/nextcloud/talk-android/releases). + +#### Alpha versions + +##### via Google Play + +To become an alpha tester you have to be signed up for the [Google Play Beta channel](https://play.google.com/apps/testing/com.nextcloud.talk2) +and additionally you have to join the [Alpha testing Google Group](https://groups.google.com/g/nextcloud-android-talk-alpha-testing). +After that you will receive the alpha versions via the Play Store (initially, this might take some minutes after + signing up). However, in the Play Store the app will still be named "Nextcloud Talk (Beta)" even if you are an alpha tester, but you will receive the alpha versions. +If a beta was released that is newer than the alpha version, you will get the beta in the alpha channel. + +##### via Download page + +In addition to google play, the alpha and beta apps can also be obtained from the Nextcloud [Download page](https://download.nextcloud.com/android/talk-alpha/) +Please make sure to remember that these versions might contain bugs and you don't use them in production. + +## Support :rescue_worker_helmet: + +If you need assistance or want to ask a question about the Talk Android app, you are welcome to [ask for community help](https://help.nextcloud.com/c/support/talk/52) in our forums. If you have found a bug, feel free to [open a new issue on GitHub](https://github.com/nextcloud/talk-android/issues). Keep in mind, that this repository only manages the Nextcloud Talk for Android app. If you find bugs or have problems with the server/backend, you should ask the [Nextcloud server team](https://github.com/nextcloud/server) for help! + +### Notifications + +If you have problems to receive talk notifications on your android phone, please have a look at [this checklist](https://github.com/nextcloud/talk-android/blob/master/docs/notifications.md). + +## Credits :scroll: + +### Ringtones :bell: + +- [Ringtones by Librem](https://developer.puri.sm/licenses/Librem5/Birch/sound-theme-librem5.html) + author: [feandesign](https://soundcloud.com/feandesign) +- [Telefon-Freiton in Deutschland nach DTAG 1 TR 110-1, Kap. 8.3](https://commons.wikimedia.org/wiki/File:1TR110-1_Kap8.3_Freiton1.ogg) + author: arvedkrynil + +[dcofile]: https://github.com/nextcloud/talk-android/blob/master/contribute/developer-certificate-of-origin +[applyalicense]: https://github.com/nextcloud/talk-android/blob/master/contribute/HowToApplyALicense.md + +## Remarks :scroll: + +Google Play and the Google Play logo are trademarks of Google Inc. \ No newline at end of file diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..56dfa91 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later +version = 1 +SPDX-PackageName = "Nextcloud Talk - Android Client" +SPDX-PackageSupplier = "Nextcloud Android team " +SPDX-PackageDownloadLocation = "https://github.com/nextcloud/talk-android" + +[[annotations]] +path = ["app/src/main/res/values-**/strings.xml", "**/.gitignore", ".idea/**", "app/src/test/resources/robolectric.properties", "app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker", "app/src/test/resources/**.json", "fastlane/metadata/**", "app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/**.json", "app/src/generic/fastlane/metadata/android/**/full_description.txt"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2017-2024 Nextcloud GmbH and Nextcloud contributors" +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = ["app/src/**/res/mipmap-**dpi/ic_launcher.png", "app/src/**/ic_launcher-web.png"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2017-2024 Nextcloud GmbH. All rights reserved. Trademarks apply, see https://nextcloud.com/trademarks/" +SPDX-License-Identifier = "GPL-3.0-or-later" + +[[annotations]] +path = "app/src/main/res/raw/tr110_1_kap8_3_freiton1.ogg" +precedence = "aggregate" +SPDX-FileCopyrightText = "2007 arvedkrynil" +SPDX-License-Identifier = "CC-BY-SA-3.0" + +[[annotations]] +path = ["app/src/main/res/raw/librem_by_feandesign_call.ogg", "app/src/main/res/raw/librem_by_feandesign_message.ogg"] +precedence = "aggregate" +SPDX-FileCopyrightText = "Feandesign" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "app/src/main/res/raw/next_voice_message_doodle.ogg" +precedence = "aggregate" +SPDX-FileCopyrightText = "2024 Paciosoft" +SPDX-License-Identifier = "CC-BY-4.0" \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4a991e1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,28 @@ + +# Security Policy + +## Supported Versions + +Only the latest major release version of Nextcloud Talk Android is currently being supported with security updates. + +## Reporting a Vulnerability + +Security is very important to us. If you have discovered a security issue with Nextcloud, +please read our responsible disclosure guidelines and contact us at [hackerone.com/nextcloud](https://hackerone.com/nextcloud). +Your report should include: + +- Product version +- A vulnerability description +- Reproduction steps + +A member of the security team will confirm the vulnerability, determine its impact, and develop a fix. +The fix will be applied to the master branch, tested, and packaged in the next security release. +The vulnerability will be publicly announced after the release. Finally, your name will be added +to the [hall of fame](https://hackerone.com/nextcloud/thanks) as a thank you from the entire Nextcloud community. Note our +[threat model](https://nextcloud.com/security/threat-model) to know what is expected behavior. + + +Please visit https://nextcloud.com/security/ for further information about security. diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..540fca7 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,132 @@ + +# [Nextcloud](https://nextcloud.com) Talk for Android Setup Guide + +# Index +1. [Setup Guide](#setup-guide) + 1. [Tooling setup](#tooling-setup) + 1. [Fork and checkout the repository](#fork-and-checkout-the-repository) + 1. [Work and build environments](#work-and-build-environments) + 1. [Working with Android Studio](#working-with-android-studio) + 1. [Working in a terminal with gradle](#working-in-a-terminal-with-gradle) + 1. [App flavours](#app-flavours) + 1. [Troubleshooting](#troubleshooting) + 1. [Compilation fails with "java.lang.OutOfMemoryError: Java heap space" error](#compilation-fails-with-javalangoutofmemoryerror-java-heap-space-error) + +# Setup Guide + +These instructions will help you to set up your development environment, get the source code of the Nextcloud Talk for Android app and build it by yourself. +If you want to help developing the app take a look to the [contribution guidelines][0]. + +Sections 1) and 2) are common for any environment. +The rest of the sections describe how to set up a project in different tool environments. +Nowadays we recommend to use Android Studio (section 2), but you can also build the app from the command line (section 3). + +If you have any problem, remove the 'talk-android' folder, start again from 1) and work your way down. +If something still does not work as described here, please open a new issue describing exactly what you did, what happened, and what should have happened. + +## Tooling setup + +There are some tools needed, no matter what is your specific IDE or build tool of preference. + +[git][1] is used to access to the different versions of the Nextcloud's source code. +Download and install the version appropriate for your operating system from [here][2]. +Add the full path to the 'bin/' directory from your git installation into the PATH variable of your environment so that it can be used from any location. + +[Android Studio][5] is currently the official Android IDE. Due to this, we recommend it as the IDE to use in your development environment. +Follow the installation instructions [here][6]. + +We recommend to use the last version available in the stable channel of Android Studio updates. + +The Android SDK is necessary to build the app. Install it via Android Studio itself: + +```Settings``` → ```Languages & Frameworks```→ ```Android SDK``` + +After installing it, add the full path to the directories 'tools/' and 'platform-tools/' from your Android SDK installation into the PATH variable of your environment. + +Open the Android SDK Manager under Android Studio's settings + +```Settings``` → ```Languages & Frameworks```→ ```Android SDK``` + +To build the Nextcloud for Android app you will need to install at least the next SDK packages: + +* Android SDK Tools and Android SDK Platform-Tools (already installed); upgrade to their last versions is usually a good idea. +* Android SDK Build-Tools 30.0.3. +* Android 11 (API 29), SDK Platform; needed to build the nextcloud app. + +Install any other package you consider interesting, such as emulators. + +After installing it, add the full path to the directories 'tools/' and 'platform-tools/' from your Android SDK installation into the PATH variable of your environment. + +## Fork and checkout the repository + +Fork and download the nextcloud/talk-android repository. + +You will need [git][1] to access to the different versions of the Nextcloud's source code. +The source code is hosted on GitHub and may be read by anybody, without a GitHub account. +You will need one if you want to contribute to the development of the app with your own code. + +The next steps will assume you have a GitHub account and that you will get the code from your own fork. + +1. In a web browser, go to https://github.com/nextcloud/talk-android, and click the 'Fork' button near the top right corner. +2. Open a terminal and go on with the next steps in it. +3. Clone your forked repository: ```git clone --recursive https://github.com/YOURGITHUBNAME/talk-android.git```. +4. Move to the project folder with ```cd talk-android```. +5. Pull any changes from your remote branch 'master': ```git pull origin master``` +6. Make official Nextcloud repo known as upstream: ```git remote add upstream https://github.com/nextcloud/talk-android.git``` +7. Make sure to get the latest changes from official talk-android/master branch: ```git pull upstream master``` + +At this point you can continue using different tools to build the project. +Sections [Working with Android Studio](#working-with-android-studio) and [Working in a terminal with gradle](#working-in-a-terminal-with-gradle) describe the existing alternatives. + +## Work and build environments + +### Working with Android Studio + +To set up the project in Android Studio follow the next steps: + +1. Open Android Studio and select 'Open an Existing Project'. Browse through your file system to the folder 'talk-android' where the project is located. The file chooser will show an Android face as the folder icon, which you can select to reopen the project. +2. Android Studio will try to build the project directly after importing it. To build it manually, just click the 'Play' button in the toolbar to build and run it in a mobile device or an emulator. The resulting APK file will be saved in the 'app/build/outputs/apk/' subdirectory in the project folder and being installed/launched in a configured emulator or if connected your physical device. +3. Check Android Studio editor configuration for the project: ```Settings``` → ```Editor``` → ```Code Style``` → ```Scheme: Project``` and ```Enable EditorConfig support``` (should be enabled by default) + +### Working in a terminal with gradle + +[Gradle][7] is the build system used by Android Studio to manage the building operations on Android apps. +You do not need to install gradle in your system, and Google recommends not to do it, but instead trusting on the [Gradle wrapper][8] included in the project. + +1. Open a terminal and go to the 'talk-android' directory that contains the repository. +2. Run the 'clean' and 'build' tasks using the gradle wrapper provided + - Windows: ```gradlew.bat clean assembleGplay``` or ```gradlew.bat clean assembleGeneric``` + - Mac OS/Linux: ```./gradlew clean assembleGplay``` or ```./gradlew clean assembleGeneric``` + +The first time the gradle wrapper is called, the correct gradle version will be downloaded automatically. +This requires a working Internet connection. + +The generated APK file is saved in ```app/build/outputs/apk``` as ```app-generic-debug.apk```. + +### App flavours + +The app is currently equipped to be built with three flavours: +* **Generic** - the regular build, released as Nextcloud Android app on F-Droid +* **Gplay** - with Google stuff (push notification), used for Google Play store +* **Qa** - build per pr for testing + +## Troubleshooting + +### Compilation fails with "java.lang.OutOfMemoryError: Java heap space" error + +The default settings for gradle is to limit the compilation to 1GB of heap. +You can increase that value by: +1. adding `org.gradle.jvmargs=-Xmx4G` to `gradle.properties` and +2. running gradlew(.bat) with this command line : `GRADLE_OPTS="-Xmx4G" ./gradlew clean build"` + +[0]: https://github.com/nextcloud/talk-android/blob/master/CONTRIBUTING.md +[1]: https://git-scm.com/ +[2]: https://git-scm.com/downloads +[5]: https://developer.android.com/studio +[6]: https://developer.android.com/studio/install +[7]: https://gradle.org/ +[8]: https://docs.gradle.org/current/userguide/gradle_wrapper.html +[9]: https://github.com/pinterest/ktlint/releases/latest diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..90e1578 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,418 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2021-2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +import com.github.spotbugs.snom.Confidence +import com.github.spotbugs.snom.Effort +import com.github.spotbugs.snom.SpotBugsTask + +plugins { + id "org.jetbrains.kotlin.plugin.compose" version "2.2.20" + id "org.jetbrains.kotlin.kapt" + id 'com.google.devtools.ksp' version '2.2.20-2.0.3' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' +apply plugin: 'com.github.spotbugs' +apply plugin: 'io.gitlab.arturbosch.detekt' +apply plugin: "org.jlleitschuh.gradle.ktlint" +apply plugin: 'kotlinx-serialization' + +android { + compileSdkVersion 35 + + namespace = 'com.nextcloud.talk' + + defaultConfig { + minSdkVersion 26 + targetSdkVersion 35 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + // mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable) + // xx .xxx .xx .xx + versionCode 230000005 + versionName "23.0.0 Alpha 05" + + flavorDimensions "default" + renderscriptTargetApi = 19 + renderscriptSupportModeEnabled true + + productFlavors { + // used for f-droid + generic { + applicationId 'com.nextcloud.talk2' + dimension "default" + } + gplay { + applicationId 'com.nextcloud.talk2' + dimension "default" + } + qa { + applicationId "com.nextcloud.talk2.qa" + dimension "default" + versionCode 1 + versionName "1" + } + } + + // Enabling multidex support. + multiDexEnabled = true + + vectorDrawables.useSupportLibrary = true + + lintOptions { + disable 'InvalidPackage' + disable 'MissingTranslation' + disable 'VectorPath' + disable 'UnusedQuantity' + } + + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } + + sourceSets { + // Adds exported schema location as test app assets. + getByName("androidTest").assets.srcDir("$projectDir/schemas") + } + + testInstrumentationRunnerArgument "TEST_SERVER_URL", "${NC_TEST_SERVER_BASEURL}" + testInstrumentationRunnerArgument "TEST_SERVER_USERNAME", "${NC_TEST_SERVER_USERNAME}" + testInstrumentationRunnerArgument "TEST_SERVER_PASSWORD", "${NC_TEST_SERVER_PASSWORD}" + + def localBroadcastPermission = "PRIVATE_BROADCAST" + manifestPlaceholders.broadcastPermission = localBroadcastPermission + buildConfigField "String", "PERMISSION_LOCAL_BROADCAST", "\"${localBroadcastPermission}\"" + } + + testOptions { + unitTests.all { + useJUnitPlatform() + } + unitTests.returnDefaultValues = true + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + packagingOptions { + resources { + excludes += [ + 'META-INF/LICENSE.txt', + 'META-INF/LICENSE', + 'META-INF/NOTICE.txt', + 'META-INF/NOTICE', + 'META-INF/DEPENDENCIES', + 'META-INF/rxjava.properties' + ] + } + } + + check.dependsOn 'spotbugsGplayDebug', 'lint', 'ktlintCheck', 'detekt' + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + buildFeatures { + viewBinding = true + buildConfig = true + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.15" + } + + lint { + abortOnError = false + disable 'MissingTranslation','PrivateResource' + htmlOutput = layout.buildDirectory.file("reports/lint/lint.html").get().asFile + htmlReport = true + } +} +kapt { + correctErrorTypes = true +} + +ext { + androidxCameraVersion = "1.5.0" + coilKtVersion = "2.7.0" + daggerVersion = "2.57.1" + emojiVersion = "1.6.0" + fidoVersion = "4.1.0-patch2" + lifecycleVersion = '2.9.4' + okhttpVersion = "4.12.0" + markwonVersion = "4.6.2" + materialDialogsVersion = "3.3.0" + parcelerVersion = "1.1.13" + prismVersion = "2.0.0" + retrofit2Version = "3.0.0" + roomVersion = "2.8.0" + workVersion = "2.10.4" + espressoVersion = "3.7.0" + androidxTestVersion = "1.5.0" + media3_version = "1.8.0" + coroutines_version = "1.10.2" + mockitoKotlinVersion = "6.0.0" +} + +configurations.configureEach { + exclude group: 'com.google.firebase', module: 'firebase-core' + exclude group: 'com.google.firebase', module: 'firebase-analytics' + exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' + exclude group: 'org.jetbrains', module: 'annotations-java5' // via prism4j, already using annotations explicitly +} + +dependencies { + implementation "androidx.room:room-testing-android:${roomVersion}" + implementation 'androidx.compose.foundation:foundation-layout:1.9.1' + spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0' + spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.14' + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.8") + + implementation("androidx.compose.runtime:runtime:1.9.1") + implementation 'androidx.preference:preference-ktx:1.2.1' + implementation 'androidx.datastore:datastore-core:1.1.7' + implementation 'androidx.datastore:datastore-preferences:1.1.7' + implementation 'androidx.test.ext:junit-ktx:1.3.0' + + implementation fileTree(include: ['*'], dir: 'libs') + + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" + + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'com.google.android.material:material:1.13.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' + implementation "com.vanniktech:emoji-google:0.21.0" + implementation "androidx.emoji2:emoji2:${emojiVersion}" + implementation "androidx.emoji2:emoji2-bundled:${emojiVersion}" + implementation "androidx.emoji2:emoji2-views:${emojiVersion}" + implementation "androidx.emoji2:emoji2-views-helper:${emojiVersion}" + implementation 'org.michaelevans.colorart:library:0.0.3' + implementation "androidx.work:work-runtime:${workVersion}" + implementation "androidx.work:work-rxjava2:${workVersion}" + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation 'com.google.android.flexbox:flexbox:3.0.0' + implementation ('com.github.bitfireAT:dav4jvm:2.1.3', { + exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser + }) + implementation 'org.conscrypt:conscrypt-android:2.5.3' + implementation "com.github.nextcloud-deps:qrcodescanner:0.1.2.4" // "com.github.blikoon:QRCodeScanner:0.1.2" + + implementation "androidx.camera:camera-core:${androidxCameraVersion}" + implementation "androidx.camera:camera-camera2:${androidxCameraVersion}" + implementation "androidx.camera:camera-lifecycle:${androidxCameraVersion}" + implementation "androidx.camera:camera-view:${androidxCameraVersion}" + implementation "androidx.exifinterface:exifinterface:1.4.1" + + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${lifecycleVersion}" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${lifecycleVersion}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${lifecycleVersion}" + implementation "androidx.lifecycle:lifecycle-process:${lifecycleVersion}" + implementation "androidx.lifecycle:lifecycle-common:${lifecycleVersion}" + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${lifecycleVersion}") + + implementation 'androidx.biometric:biometric:1.1.0' + + implementation 'androidx.multidex:multidex:2.0.1' + + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation "io.reactivex.rxjava2:rxjava:2.2.21" + + implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}" + implementation "com.squareup.okhttp3:okhttp-urlconnection:${okhttpVersion}" + implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}" + + implementation 'com.bluelinelabs:logansquare:1.3.7' + implementation 'com.fasterxml.jackson.core:jackson-core:2.20.0' + kapt 'com.bluelinelabs:logansquare-compiler:1.3.7' + + implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}" + implementation "com.squareup.retrofit2:adapter-rxjava2:${retrofit2Version}" + implementation "com.squareup.retrofit2:converter-gson:${retrofit2Version}" + implementation 'de.mannodermaus.retrofit2:converter-logansquare:1.4.1' + + implementation "com.google.dagger:dagger:${daggerVersion}" + kapt "com.google.dagger:dagger-compiler:${daggerVersion}" + implementation 'com.github.lukaspili.autodagger2:autodagger2:1.1' + kapt 'com.github.lukaspili.autodagger2:autodagger2-compiler:1.1' + compileOnly 'javax.annotation:javax.annotation-api:1.3.2' + + implementation 'org.greenrobot:eventbus:3.3.1' + implementation 'net.zetetic:sqlcipher-android:4.10.0' + + implementation "androidx.room:room-runtime:${roomVersion}" + implementation "androidx.room:room-rxjava2:${roomVersion}" + ksp "androidx.room:room-compiler:${roomVersion}" + implementation "androidx.room:room-ktx:${roomVersion}" + + implementation "org.parceler:parceler-api:$parcelerVersion" + implementation 'com.github.ddB0515.FlexibleAdapter:flexible-adapter:5.1.1' + implementation 'com.github.ddB0515.FlexibleAdapter:flexible-adapter-ui:5.1.1' + implementation 'org.apache.commons:commons-lang3:3.18.0' + implementation 'com.github.wooplr:Spotlight:1.3' + implementation 'com.google.code.findbugs:jsr305:3.0.2' + implementation 'com.github.nextcloud-deps:ChatKit:0.4.2' + implementation 'joda-time:joda-time:2.14.0' + implementation "io.coil-kt:coil:${coilKtVersion}" + implementation "io.coil-kt:coil-gif:${coilKtVersion}" + implementation "io.coil-kt:coil-svg:${coilKtVersion}" + implementation "io.coil-kt:coil-compose:${coilKtVersion}" + implementation 'com.github.natario1:Autocomplete:1.1.0' + + implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido:${fidoVersion}" + implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido2:${fidoVersion}" + + implementation "com.afollestad.material-dialogs:core:${materialDialogsVersion}" + implementation "com.afollestad.material-dialogs:datetime:${materialDialogsVersion}" + implementation "com.afollestad.material-dialogs:bottomsheets:${materialDialogsVersion}" + implementation "com.afollestad.material-dialogs:lifecycle:${materialDialogsVersion}" + + implementation 'com.google.code.gson:gson:2.13.2' + + implementation "androidx.media3:media3-exoplayer:$media3_version" + implementation "androidx.media3:media3-ui:$media3_version" + + implementation 'com.github.chrisbanes:PhotoView:2.3.0' + implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.29' + + implementation "io.noties.markwon:core:$markwonVersion" + implementation "io.noties.markwon:ext-strikethrough:$markwonVersion" + implementation "io.noties.markwon:ext-tasklist:$markwonVersion" + implementation "io.noties.markwon:ext-tables:$markwonVersion" + + implementation 'com.github.nextcloud-deps:ImagePicker:2.1.0.2' + implementation 'io.github.elye:loaderviewlibrary:3.0.0' + implementation 'org.osmdroid:osmdroid-android:6.1.20' + implementation ('fr.dudie:nominatim-api:3.4', { + //noinspection DuplicatePlatformClasses + exclude group: 'org.apache.httpcomponents', module: 'httpclient' + }) + + implementation 'androidx.core:core-ktx:1.16.0' + implementation 'androidx.activity:activity-ktx:1.10.1' + implementation 'com.github.nextcloud.android-common:ui:0.28.0' + implementation 'com.github.nextcloud-deps:android-talk-webrtc:132.6834.0' + + gplayImplementation 'com.google.android.gms:play-services-base:18.8.0' + gplayImplementation "com.google.firebase:firebase-messaging:25.0.0" + + //compose + implementation(platform("androidx.compose:compose-bom:2025.09.00")) + implementation("androidx.compose.ui:ui") + implementation 'androidx.compose.material3:material3:1.3.2' + implementation("androidx.compose.ui:ui-tooling-preview") + implementation 'androidx.activity:activity-compose:1.10.1' + debugImplementation("androidx.compose.ui:ui-tooling") + + //tests + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.13.4' + androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.9.1") + debugImplementation("androidx.compose.ui:ui-test-manifest") + + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:5.19.0' + testImplementation 'androidx.arch.core:core-testing:2.2.0' + + androidTestImplementation "androidx.test:core:1.7.0" + + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2" + androidTestImplementation 'androidx.test:core-ktx:1.7.0' + androidTestImplementation 'org.mockito:mockito-android:5.19.0' + androidTestImplementation "androidx.work:work-testing:${workVersion}" + // Espresso core + androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", { + exclude group: 'com.android.support', module: 'support-annotations' + }) + androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" + androidTestImplementation "androidx.test.espresso:espresso-web:$espressoVersion" + androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espressoVersion" + + androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" + + androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.2') + + androidTestImplementation(platform("androidx.compose:compose-bom:2025.09.00")) + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" + + testImplementation 'org.junit.vintage:junit-vintage-engine:5.13.4' // DO NOT REMOVE + testImplementation "androidx.room:room-testing:${roomVersion}" + testImplementation("com.squareup.okhttp3:mockwebserver:$okhttpVersion") + testImplementation("com.google.dagger:hilt-android-testing:2.57.1") + testImplementation("org.robolectric:robolectric:4.16") +} + +tasks.register('installGitHooks', Copy) { + description = "Install git hooks" + from("../scripts/hooks") { + include '*' + } + into '../.git/hooks' +} + +spotbugs { + ignoreFailures = true // should continue checking + effort = Effort.MAX + reportLevel = Confidence.valueOf('MEDIUM') +} + +tasks.withType(SpotBugsTask).configureEach { task -> + String variantNameCap = task.name.replace("spotbugs", "") + String variantName = variantNameCap.substring(0, 1).toLowerCase() + variantNameCap.substring(1) + + dependsOn "compile${variantNameCap}Sources" + + excludeFilter = file("${project.rootDir}/spotbugs-filter.xml") + classes = fileTree(layout.buildDirectory.get().asFile.toString()+"/intermediates/javac/${variantName}/compile${variantNameCap}JavaWithJavac/classes/") + reports { + xml { + required = true + } + html { + required = true + outputLocation = layout.buildDirectory.file("reports/spotbugs/spotbugs.html") + stylesheet = 'fancy.xsl' + } + } +} + +tasks.named("detekt").configure { + reports { + html.required.set(true) + txt.required.set(true) + xml.required.set(false) + sarif.required.set(false) + md.required.set(false) + } +} + +detekt { + config.setFrom("../detekt.yml") + source.setFrom("src/") +} + +ksp { + arg('room.schemaLocation', "$projectDir/schemas") +} \ No newline at end of file diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 0000000..60b314e --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..6731f9b --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,27 @@ +# +# Nextcloud Talk - Android Client +# +# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json new file mode 100644 index 0000000..84c3039 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/10.json @@ -0,0 +1,146 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "1b2dab0ea495c45c9c9ee6e64ba74039", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1b2dab0ea495c45c9c9ee6e64ba74039')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/11.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/11.json new file mode 100644 index 0000000..d021477 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/11.json @@ -0,0 +1,719 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "bc802cadfdef41d3eb94ffbb0729eb89", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bc802cadfdef41d3eb94ffbb0729eb89')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/12.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/12.json new file mode 100644 index 0000000..05c1320 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/12.json @@ -0,0 +1,725 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "7edb537b6987d0de6586a6760c970958", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7edb537b6987d0de6586a6760c970958')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json new file mode 100644 index 0000000..2e23450 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/13.json @@ -0,0 +1,749 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "a521f027909f69f4c7d1855f84a2e67f", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sendingFailed", + "columnName": "sendingFailed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a521f027909f69f4c7d1855f84a2e67f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/14.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/14.json new file mode 100644 index 0000000..cc73f1e --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/14.json @@ -0,0 +1,719 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "506abc931eb3b657cafe6ad1b25f635d", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT" + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT" + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT" + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT" + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + } + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT" + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT" + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT" + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT" + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT" + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT" + }, + { + "fieldPath": "sendingFailed", + "columnName": "sendingFailed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '506abc931eb3b657cafe6ad1b25f635d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/15.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/15.json new file mode 100644 index 0000000..f271e11 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/15.json @@ -0,0 +1,725 @@ +{ + "formatVersion": 1, + "database": { + "version": 15, + "identityHash": "acac3fd21e35762b90f65f213be38ccd", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT" + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT" + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT" + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT" + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + } + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT" + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT" + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasSensitive", + "columnName": "hasSensitive", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT" + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT" + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT" + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT" + }, + { + "fieldPath": "sendingFailed", + "columnName": "sendingFailed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'acac3fd21e35762b90f65f213be38ccd')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/16.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/16.json new file mode 100644 index 0000000..ba7c399 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/16.json @@ -0,0 +1,731 @@ +{ + "formatVersion": 1, + "database": { + "version": 16, + "identityHash": "bbf526d5c78a99eb951635cc46f4c59f", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT" + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT" + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT" + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT" + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + } + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT" + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT" + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasSensitive", + "columnName": "hasSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasImportant", + "columnName": "hasImportant", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT" + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT" + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT" + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT" + }, + { + "fieldPath": "sendingFailed", + "columnName": "sendingFailed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bbf526d5c78a99eb951635cc46f4c59f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/17.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/17.json new file mode 100644 index 0000000..e1abb2b --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/17.json @@ -0,0 +1,730 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "5bc4247e179307faa995552da5d34324", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT" + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT" + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT" + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT" + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + } + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT" + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT" + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasSensitive", + "columnName": "hasSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasImportant", + "columnName": "hasImportant", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT" + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT" + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT" + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT" + }, + { + "fieldPath": "sendStatus", + "columnName": "sendStatus", + "affinity": "TEXT" + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bc4247e179307faa995552da5d34324')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/18.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/18.json new file mode 100644 index 0000000..391c430 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/18.json @@ -0,0 +1,746 @@ +{ + "formatVersion": 1, + "database": { + "version": 18, + "identityHash": "c5e3716925065d7419fb23efabf6691f", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT" + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT" + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT" + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT" + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + } + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT" + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT" + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasSensitive", + "columnName": "hasSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasImportant", + "columnName": "hasImportant", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `threadId` INTEGER, `isThread` INTEGER NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "threadId", + "columnName": "threadId", + "affinity": "INTEGER" + }, + { + "fieldPath": "isThread", + "columnName": "isThread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT" + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT" + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT" + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT" + }, + { + "fieldPath": "sendStatus", + "columnName": "sendStatus", + "affinity": "TEXT" + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `threadId` INTEGER, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "threadId", + "columnName": "threadId", + "affinity": "INTEGER" + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c5e3716925065d7419fb23efabf6691f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/19.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/19.json new file mode 100644 index 0000000..c11f384 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/19.json @@ -0,0 +1,746 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "c5e3716925065d7419fb23efabf6691f", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT" + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT" + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT" + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT" + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + } + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT" + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT" + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasSensitive", + "columnName": "hasSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasImportant", + "columnName": "hasImportant", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `threadId` INTEGER, `isThread` INTEGER NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "threadId", + "columnName": "threadId", + "affinity": "INTEGER" + }, + { + "fieldPath": "isThread", + "columnName": "isThread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT" + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT" + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT" + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT" + }, + { + "fieldPath": "sendStatus", + "columnName": "sendStatus", + "affinity": "TEXT" + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `threadId` INTEGER, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "threadId", + "columnName": "threadId", + "affinity": "INTEGER" + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c5e3716925065d7419fb23efabf6691f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/20.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/20.json new file mode 100644 index 0000000..68198eb --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/20.json @@ -0,0 +1,751 @@ +{ + "formatVersion": 1, + "database": { + "version": 20, + "identityHash": "7330dad871a0b42e36931ffe8c7d4bcf", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT" + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT" + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT" + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT" + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + } + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, `messageDraft` TEXT, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT" + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT" + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasSensitive", + "columnName": "hasSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasImportant", + "columnName": "hasImportant", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageDraft", + "columnName": "messageDraft", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `threadId` INTEGER, `isThread` INTEGER NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "threadId", + "columnName": "threadId", + "affinity": "INTEGER" + }, + { + "fieldPath": "isThread", + "columnName": "isThread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT" + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT" + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT" + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT" + }, + { + "fieldPath": "sendStatus", + "columnName": "sendStatus", + "affinity": "TEXT" + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `threadId` INTEGER, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "threadId", + "columnName": "threadId", + "affinity": "INTEGER" + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7330dad871a0b42e36931ffe8c7d4bcf')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/21.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/21.json new file mode 100644 index 0000000..ebd9449 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/21.json @@ -0,0 +1,761 @@ +{ + "formatVersion": 1, + "database": { + "version": 21, + "identityHash": "8077a29304b3d28882e4b37fb10d0081", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT" + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT" + }, + { + "fieldPath": "serverVersion", + "columnName": "serverVersion", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT" + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT" + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + } + }, + { + "tableName": "Conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, `messageDraft` TEXT, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarVersion", + "columnName": "avatarVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "callFlag", + "columnName": "callFlag", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callRecording", + "columnName": "callRecording", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "callStartTime", + "columnName": "callStartTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canDeleteConversation", + "columnName": "canDeleteConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canLeaveConversation", + "columnName": "canLeaveConversation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canStartCall", + "columnName": "canStartCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasCall", + "columnName": "hasCall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasPassword", + "columnName": "hasPassword", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasCustomAvatar", + "columnName": "isCustomAvatar", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "isFavorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastActivity", + "columnName": "lastActivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCommonReadMessage", + "columnName": "lastCommonReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessage", + "columnName": "lastMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "lastPing", + "columnName": "lastPing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReadMessage", + "columnName": "lastReadMessage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lobbyState", + "columnName": "lobbyState", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lobbyTimer", + "columnName": "lobbyTimer", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageExpiration", + "columnName": "messageExpiration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationCalls", + "columnName": "notificationCalls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLevel", + "columnName": "notificationLevel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectType", + "columnName": "objectType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectId", + "columnName": "objectId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "participantType", + "columnName": "participantType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "conversationReadOnlyState", + "columnName": "readOnly", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recordingConsentRequired", + "columnName": "recordingConsent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteServer", + "columnName": "remoteServer", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteToken", + "columnName": "remoteToken", + "affinity": "TEXT" + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "statusClearAt", + "columnName": "statusClearAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "statusIcon", + "columnName": "statusIcon", + "affinity": "TEXT" + }, + { + "fieldPath": "statusMessage", + "columnName": "statusMessage", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unreadMention", + "columnName": "unreadMention", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMentionDirect", + "columnName": "unreadMentionDirect", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadMessages", + "columnName": "unreadMessages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasArchived", + "columnName": "hasArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasSensitive", + "columnName": "hasSensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasImportant", + "columnName": "hasImportant", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageDraft", + "columnName": "messageDraft", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_Conversations_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)" + } + ], + "foreignKeys": [ + { + "table": "User", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ChatMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `threadId` INTEGER, `isThread` INTEGER NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `threadTitle` TEXT, `threadReplies` INTEGER, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "internalId", + "columnName": "internalId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "threadId", + "columnName": "threadId", + "affinity": "INTEGER" + }, + { + "fieldPath": "isThread", + "columnName": "isThread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "actorDisplayName", + "columnName": "actorDisplayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorId", + "columnName": "actorId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorType", + "columnName": "actorType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTimestamp", + "columnName": "expirationTimestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyable", + "columnName": "isReplyable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTemporary", + "columnName": "isTemporary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastEditActorDisplayName", + "columnName": "lastEditActorDisplayName", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorId", + "columnName": "lastEditActorId", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditActorType", + "columnName": "lastEditActorType", + "affinity": "TEXT" + }, + { + "fieldPath": "lastEditTimestamp", + "columnName": "lastEditTimestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "renderMarkdown", + "columnName": "markdown", + "affinity": "INTEGER" + }, + { + "fieldPath": "messageParameters", + "columnName": "messageParameters", + "affinity": "TEXT" + }, + { + "fieldPath": "messageType", + "columnName": "messageType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentMessageId", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "reactions", + "columnName": "reactions", + "affinity": "TEXT" + }, + { + "fieldPath": "reactionsSelf", + "columnName": "reactionsSelf", + "affinity": "TEXT" + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "TEXT" + }, + { + "fieldPath": "sendStatus", + "columnName": "sendStatus", + "affinity": "TEXT" + }, + { + "fieldPath": "silent", + "columnName": "silent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "systemMessageType", + "columnName": "systemMessage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "threadTitle", + "columnName": "threadTitle", + "affinity": "TEXT" + }, + { + "fieldPath": "threadReplies", + "columnName": "threadReplies", + "affinity": "INTEGER" + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "internalId" + ] + }, + "indices": [ + { + "name": "index_ChatMessages_internalId", + "unique": true, + "columnNames": [ + "internalId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)" + }, + { + "name": "index_ChatMessages_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + }, + { + "tableName": "ChatBlocks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `threadId` INTEGER, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalConversationId", + "columnName": "internalConversationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "threadId", + "columnName": "threadId", + "affinity": "INTEGER" + }, + { + "fieldPath": "oldestMessageId", + "columnName": "oldestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "newestMessageId", + "columnName": "newestMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasHistory", + "columnName": "hasHistory", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ChatBlocks_internalConversationId", + "unique": false, + "columnNames": [ + "internalConversationId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)" + } + ], + "foreignKeys": [ + { + "table": "Conversations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "internalConversationId" + ], + "referencedColumns": [ + "internalId" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8077a29304b3d28882e4b37fb10d0081')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/8.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/8.json new file mode 100644 index 0000000..68394c1 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/8.json @@ -0,0 +1,138 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "055a9d64f28216e2981bea2fb6cc4b28", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "accountIdentifier" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '055a9d64f28216e2981bea2fb6cc4b28')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/9.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/9.json new file mode 100644 index 0000000..4b966b3 --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/9.json @@ -0,0 +1,139 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "666fcc4bbbdf3ff121b8f1ace8fcbcb8", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "accountIdentifier", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '666fcc4bbbdf3ff121b8f1ace8fcbcb8')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/nextcloud/talk/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/nextcloud/talk/ExampleInstrumentedTest.java new file mode 100644 index 0000000..72e1a37 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/ExampleInstrumentedTest.java @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk; + +import android.content.Context; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertNotNull(appContext.getPackageName()); + assertTrue("The package name must start with 'com.nextcloud.talk2'", appContext.getPackageName().startsWith("com.nextcloud.talk2")); + } +} diff --git a/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt b/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt new file mode 100644 index 0000000..498c401 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt @@ -0,0 +1,48 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tobias Kaminsky + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.activities + +import androidx.test.espresso.intent.rule.IntentsTestRule +import com.nextcloud.talk.users.UserManager +import org.junit.Assert.assertNotNull +import org.junit.Rule +import org.junit.Test + +class MainActivityTest { + @get:Rule + val activityRule: IntentsTestRule = IntentsTestRule( + MainActivity::class.java, + true, + false + ) + + @Test + fun login() { + val sut = activityRule.launchActivity(null) + + val user = sut.userManager.storeProfile( + "test", + UserManager.UserAttributes( + null, + serverUrl = "http://server/nc", + currentUser = true, + userId = "test", + token = "test", + displayName = "Test Name", + pushConfigurationState = null, + capabilities = null, + serverVersion = null, + certificateAlias = null, + externalSignalingServer = null + ) + ).blockingGet() + + assertNotNull("Error creating user", user) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt new file mode 100644 index 0000000..01067b1 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatBlocksDaoTest.kt @@ -0,0 +1,393 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.dao + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.runner.AndroidJUnit4 +import com.nextcloud.talk.data.database.model.ChatBlockEntity +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.data.source.local.TalkDatabase +import com.nextcloud.talk.data.user.UsersDao +import com.nextcloud.talk.data.user.model.UserEntity +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.lang.Boolean +import kotlin.Long +import kotlin.String + +@RunWith(AndroidJUnit4::class) +class ChatBlocksDaoTest { + private lateinit var usersDao: UsersDao + private lateinit var conversationsDao: ConversationsDao + private lateinit var chatBlocksDao: ChatBlocksDao + private lateinit var db: TalkDatabase + private val tag = ChatBlocksDaoTest::class.java.simpleName + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, + TalkDatabase::class.java + ).build() + usersDao = db.usersDao() + conversationsDao = db.conversationsDao() + chatBlocksDao = db.chatBlocksDao() + } + + @After + fun closeDb() = db.close() + + @Test + fun testGetChatBlocksContainingMessageId() = + runTest { + val user = createUserEntity("account1", "Account 1") + usersDao.saveUser(user) + val account1 = usersDao.getUserWithUserId("account1").blockingGet() + + conversationsDao.upsertConversations( + accountId = user.id, + listOf( + createConversationEntity( + accountId = account1.id, + "abc", + roomName = "Conversation One" + ), + createConversationEntity( + accountId = account1.id, + "def", + roomName = "Conversation Two" + ) + ) + ) + + val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0] + + val chatBlock1 = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = 123, + oldestMessageId = 50, + newestMessageId = 60, + hasHistory = true + ) + + val chatBlock2 = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = 123, + oldestMessageId = 10, + newestMessageId = 20, + hasHistory = true + ) + + val chatBlock3 = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = null, + oldestMessageId = 50, + newestMessageId = 60, + hasHistory = true + ) + + chatBlocksDao.upsertChatBlock(chatBlock1) + chatBlocksDao.upsertChatBlock(chatBlock2) + chatBlocksDao.upsertChatBlock(chatBlock3) + + val chatBlocksOfThread = chatBlocksDao.getChatBlocksContainingMessageId( + internalConversationId = conversation1.internalId, + threadId = 123, + messageId = 55 + ) + + assertEquals(1, chatBlocksOfThread.first().size) + } + + @Test + fun testGetConnectedChatBlocks() = + runTest { + val user = createUserEntity("account1", "Account 1") + usersDao.saveUser(user) + val account1 = usersDao.getUserWithUserId("account1").blockingGet() + + conversationsDao.upsertConversations( + account1.id, + listOf( + createConversationEntity( + accountId = account1.id, + "abc", + roomName = "Conversation One" + ), + createConversationEntity( + accountId = account1.id, + "def", + roomName = "Conversation Two" + ) + ) + ) + + val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0] + val conversation2 = conversationsDao.getConversationsForUser(account1.id).first()[1] + + val searchedChatBlock = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = null, + oldestMessageId = 50, + newestMessageId = 60, + hasHistory = true + ) + + val chatBlockTooOld = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = null, + oldestMessageId = 10, + newestMessageId = 20, + hasHistory = true + ) + + val chatBlockOverlap1 = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = null, + oldestMessageId = 45, + newestMessageId = 55, + hasHistory = true + ) + + val chatBlockWithin = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = null, + oldestMessageId = 52, + newestMessageId = 58, + hasHistory = true + ) + + val chatBlockOverall = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = null, + oldestMessageId = 1, + newestMessageId = 99, + hasHistory = true + ) + + val chatBlockOverlap2 = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = null, + oldestMessageId = 59, + newestMessageId = 70, + hasHistory = true + ) + + val chatBlockTooNew = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = null, + oldestMessageId = 80, + newestMessageId = 90, + hasHistory = true + ) + + val chatBlockWithinButOtherConversation = ChatBlockEntity( + internalConversationId = conversation2.internalId, + accountId = conversation2.accountId, + token = conversation2.token, + threadId = null, + oldestMessageId = 53, + newestMessageId = 57, + hasHistory = true + ) + + chatBlocksDao.upsertChatBlock(searchedChatBlock) + + chatBlocksDao.upsertChatBlock(chatBlockTooOld) + chatBlocksDao.upsertChatBlock(chatBlockOverlap1) + chatBlocksDao.upsertChatBlock(chatBlockWithin) + chatBlocksDao.upsertChatBlock(chatBlockOverall) + chatBlocksDao.upsertChatBlock(chatBlockOverlap2) + chatBlocksDao.upsertChatBlock(chatBlockTooNew) + chatBlocksDao.upsertChatBlock(chatBlockWithinButOtherConversation) + + val results = chatBlocksDao.getConnectedChatBlocks( + internalConversationId = conversation1.internalId, + threadId = null, + oldestMessageId = searchedChatBlock.oldestMessageId, + newestMessageId = searchedChatBlock.newestMessageId + ) + + assertEquals(5, results.first().size) + } + + @Test + fun testGetConnectedChatBlocksWithThreadsScenario() = + runTest { + val user = createUserEntity("account1", "Account 1") + usersDao.saveUser(user) + val account1 = usersDao.getUserWithUserId("account1").blockingGet() + + conversationsDao.upsertConversations( + account1.id, + listOf( + createConversationEntity( + accountId = account1.id, + "abc", + roomName = "Conversation One" + ), + createConversationEntity( + accountId = account1.id, + "def", + roomName = "Conversation Two" + ) + ) + ) + + val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0] + + val searchedChatBlock = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = 123, + oldestMessageId = 50, + newestMessageId = 60, + hasHistory = true + ) + + val chatBlockOverlap1 = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = null, + oldestMessageId = 45, + newestMessageId = 55, + hasHistory = true + ) + + val chatBlockOverlap2 = ChatBlockEntity( + internalConversationId = conversation1.internalId, + accountId = conversation1.accountId, + token = conversation1.token, + threadId = 123, + oldestMessageId = 59, + newestMessageId = 70, + hasHistory = true + ) + + chatBlocksDao.upsertChatBlock(searchedChatBlock) + + chatBlocksDao.upsertChatBlock(chatBlockOverlap1) + chatBlocksDao.upsertChatBlock(chatBlockOverlap2) + + val resultsForThreadIdNull = chatBlocksDao.getConnectedChatBlocks( + internalConversationId = conversation1.internalId, + threadId = null, + oldestMessageId = searchedChatBlock.oldestMessageId, + newestMessageId = searchedChatBlock.newestMessageId + ) + + assertEquals(1, resultsForThreadIdNull.first().size) + + val resultsForThreadId123 = chatBlocksDao.getConnectedChatBlocks( + internalConversationId = conversation1.internalId, + threadId = 123, + oldestMessageId = searchedChatBlock.oldestMessageId, + newestMessageId = searchedChatBlock.newestMessageId + ) + + assertEquals(2, resultsForThreadId123.first().size) + } + + private fun createUserEntity(userId: String, userName: String) = + UserEntity( + userId = userId, + username = userName, + baseUrl = null, + token = null, + displayName = null, + pushConfigurationState = null, + capabilities = null, + serverVersion = null, + clientCertificate = null, + externalSignalingServer = null, + current = Boolean.FALSE, + scheduledForDeletion = Boolean.FALSE + ) + + private fun createConversationEntity(accountId: Long, token: String, roomName: String) = + ConversationEntity( + internalId = "$accountId@$token", + accountId = accountId, + token = token, + name = roomName, + actorId = "", + actorType = "", + messageExpiration = 0, + unreadMessages = 0, + statusMessage = null, + lastMessage = null, + canDeleteConversation = false, + canLeaveConversation = false, + lastCommonReadMessage = 0, + lastReadMessage = 0, + type = ConversationEnums.ConversationType.DUMMY, + status = "", + callFlag = 1, + favorite = false, + lastPing = 0, + hasCall = false, + sessionId = "", + canStartCall = false, + lastActivity = 0, + remoteServer = "", + avatarVersion = "", + unreadMentionDirect = false, + callRecording = 1, + callStartTime = 0, + statusClearAt = 0, + unreadMention = false, + lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY, + lobbyTimer = 0, + objectType = ConversationEnums.ObjectType.FILE, + objectId = "", + statusIcon = null, + description = "", + displayName = "", + hasPassword = false, + permissions = 0, + notificationCalls = 0, + remoteToken = "", + notificationLevel = ConversationEnums.NotificationLevel.ALWAYS, + conversationReadOnlyState = ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY, + hasCustomAvatar = false, + participantType = Participant.ParticipantType.DUMMY, + recordingConsentRequired = 1 + ) +} diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt new file mode 100644 index 0000000..6bcce26 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/dao/ChatMessagesDaoTest.kt @@ -0,0 +1,276 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.dao + +import android.content.Context +import android.util.Log +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.runner.AndroidJUnit4 +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.data.source.local.TalkDatabase +import com.nextcloud.talk.data.user.UsersDao +import com.nextcloud.talk.data.user.model.UserEntity +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ChatMessagesDaoTest { + + private lateinit var usersDao: UsersDao + private lateinit var conversationsDao: ConversationsDao + private lateinit var chatMessagesDao: ChatMessagesDao + private lateinit var db: TalkDatabase + private val tag = ChatMessagesDaoTest::class.java.simpleName + + var chatMessageCounter: Long = 1 + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, + TalkDatabase::class.java + ).build() + usersDao = db.usersDao() + conversationsDao = db.conversationsDao() + chatMessagesDao = db.chatMessagesDao() + } + + @After + fun closeDb() = db.close() + + @Test + fun test() = + runTest { + usersDao.saveUser(createUserEntity("account1", "Account 1")) + usersDao.saveUser(createUserEntity("account2", "Account 2")) + + val account1 = usersDao.getUserWithUserId("account1").blockingGet() + val account2 = usersDao.getUserWithUserId("account2").blockingGet() + + // Problem: lets say we want to update the conv list -> We don#t know the primary keys! + // with account@token that would be easier! + conversationsDao.upsertConversations( + account1.id, + listOf( + createConversationEntity( + accountId = account1.id, + roomName = "Conversation One" + ), + createConversationEntity( + accountId = account1.id, + roomName = "Conversation Two" + ), + createConversationEntity( + accountId = account2.id, + roomName = "Conversation Three" + ) + ) + ) + + assertEquals(2, conversationsDao.getConversationsForUser(account1.id).first().size) + assertEquals(1, conversationsDao.getConversationsForUser(account2.id).first().size) + + // Lets imagine we are on conversations screen... + conversationsDao.getConversationsForUser(account1.id).first().forEach { + Log.d(tag, "- next Conversation for account1 -") + Log.d(tag, "internalId (PK): " + it.internalId) + Log.d(tag, "accountId: " + it.accountId) + Log.d(tag, "name: " + it.name) + Log.d(tag, "token: " + it.token) + } + + // User sees all conversations and clicks on a item. That's how we get a conversation + val conversation1 = conversationsDao.getConversationsForUser(account1.id).first()[0] + val conversation2 = conversationsDao.getConversationsForUser(account1.id).first()[1] + + // Having a conversation token, we can also get a conversation directly + val conversation1GotByToken = conversationsDao.getConversationForUser( + account1.id, + conversation1.token!! + ).first() + + assertEquals(conversation1, conversation1GotByToken) + + // Lets insert some messages to the conversations + chatMessagesDao.upsertChatMessages( + listOf( + createChatMessageEntity(conversation1.internalId, "hello"), + createChatMessageEntity(conversation1.internalId, "here"), + createChatMessageEntity(conversation1.internalId, "are"), + createChatMessageEntity(conversation1.internalId, "some"), + createChatMessageEntity(conversation1.internalId, "messages") + ) + ) + chatMessagesDao.upsertChatMessages( + listOf( + createChatMessageEntity(conversation2.internalId, "first message in conversation 2") + ) + ) + + chatMessagesDao.getMessagesForConversation(conversation1.internalId).first().forEach { + Log.d(tag, "- next Message for conversation1 (account1)-") + Log.d(tag, "id (PK): " + it.id) + Log.d(tag, "message: " + it.message) + } + + val chatMessagesConv1 = chatMessagesDao.getMessagesForConversation(conversation1.internalId) + assertEquals(5, chatMessagesConv1.first().size) + + val chatMessagesConv2 = chatMessagesDao.getMessagesForConversation(conversation2.internalId) + assertEquals(1, chatMessagesConv2.first().size) + + assertEquals("some", chatMessagesConv1.first()[1].message) + + val conv1chatMessage3 = chatMessagesDao.getChatMessageForConversation(conversation1.internalId, 3).first() + assertEquals("are", conv1chatMessage3.message) + + val chatMessagesConv1Since = + chatMessagesDao.getMessagesForConversationSince( + conversation1.internalId, + conv1chatMessage3.id, + null + ) + assertEquals(3, chatMessagesConv1Since.first().size) + assertEquals("are", chatMessagesConv1Since.first()[0].message) + assertEquals("some", chatMessagesConv1Since.first()[1].message) + assertEquals("messages", chatMessagesConv1Since.first()[2].message) + + val chatMessagesConv1To = + chatMessagesDao.getMessagesForConversationBeforeAndEqual( + conversation1.internalId, + conv1chatMessage3.id, + 3, + null + ) + assertEquals(3, chatMessagesConv1To.first().size) + assertEquals("hello", chatMessagesConv1To.first()[2].message) + assertEquals("here", chatMessagesConv1To.first()[1].message) + assertEquals("are", chatMessagesConv1To.first()[0].message) + } + + private fun createUserEntity(userId: String, userName: String) = + UserEntity( + userId = userId, + username = userName, + baseUrl = null, + token = null, + displayName = null, + pushConfigurationState = null, + capabilities = null, + serverVersion = null, + clientCertificate = null, + externalSignalingServer = null, + current = java.lang.Boolean.FALSE, + scheduledForDeletion = java.lang.Boolean.FALSE + ) + + private fun createConversationEntity(accountId: Long, roomName: String): ConversationEntity { + val token = (0..10000000).random().toString() + + return ConversationEntity( + internalId = "$accountId@$token", + accountId = accountId, + token = token, + name = roomName, + actorId = "", + actorType = "", + messageExpiration = 0, + unreadMessages = 0, + statusMessage = null, + lastMessage = null, + canDeleteConversation = false, + canLeaveConversation = false, + lastCommonReadMessage = 0, + lastReadMessage = 0, + type = ConversationEnums.ConversationType.DUMMY, + status = "", + callFlag = 1, + favorite = false, + lastPing = 0, + hasCall = false, + sessionId = "", + canStartCall = false, + lastActivity = 0, + remoteServer = "", + avatarVersion = "", + unreadMentionDirect = false, + callRecording = 1, + callStartTime = 0, + statusClearAt = 0, + unreadMention = false, + lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY, + lobbyTimer = 0, + objectType = ConversationEnums.ObjectType.FILE, + objectId = "", + statusIcon = null, + description = "", + displayName = "", + hasPassword = false, + permissions = 0, + notificationCalls = 0, + remoteToken = "", + notificationLevel = ConversationEnums.NotificationLevel.ALWAYS, + conversationReadOnlyState = ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY, + hasCustomAvatar = false, + participantType = Participant.ParticipantType.DUMMY, + recordingConsentRequired = 1 + ) + } + + private fun createChatMessageEntity(internalConversationId: String, message: String): ChatMessageEntity { + val id = chatMessageCounter++ + + val emoji1 = "\uD83D\uDE00" // 😀 + val emoji2 = "\uD83D\uDE1C" // 😜 + val reactions = LinkedHashMap() + reactions[emoji1] = 3 + reactions[emoji2] = 4 + + val reactionsSelf = ArrayList() + reactionsSelf.add(emoji1) + + val entity = ChatMessageEntity( + internalId = "$internalConversationId@$id", + internalConversationId = internalConversationId, + id = id, + message = message, + reactions = reactions, + reactionsSelf = reactionsSelf, + deleted = false, + token = "", + actorId = "", + actorType = "", + accountId = 1, + messageParameters = null, + messageType = "", + parentMessageId = null, + systemMessageType = ChatMessage.SystemMessageType.DUMMY, + replyable = false, + timestamp = 0, + expirationTimestamp = 0, + actorDisplayName = "", + lastEditActorType = null, + lastEditTimestamp = null, + renderMarkdown = true, + lastEditActorId = "", + lastEditActorDisplayName = "" + ) + return entity + } +} diff --git a/app/src/androidTest/java/com/nextcloud/talk/data/database/migrations/MigrationsTest.kt b/app/src/androidTest/java/com/nextcloud/talk/data/database/migrations/MigrationsTest.kt new file mode 100644 index 0000000..978bab2 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/data/database/migrations/MigrationsTest.kt @@ -0,0 +1,114 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.migrations + +import androidx.room.Room +import androidx.room.testing.MigrationTestHelper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.nextcloud.talk.data.source.local.Migrations +import com.nextcloud.talk.data.source.local.TalkDatabase +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class MigrationsTest { + companion object { + private const val TEST_DB = "migration-test" + private const val INIT_VERSION = 10 // last version before update to offline first + private val TAG = MigrationsTest::class.java.simpleName + } + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + TalkDatabase::class.java + ) + + @Test + @Throws(IOException::class) + @Suppress("SpreadOperator") + fun migrateAll() { + helper.createDatabase(TEST_DB, INIT_VERSION).apply { + close() + } + + Room.databaseBuilder( + InstrumentationRegistry.getInstrumentation().targetContext, + TalkDatabase::class.java, + TEST_DB + ).addMigrations(*TalkDatabase.MIGRATIONS).build().apply { + openHelper.writableDatabase.close() + } + } + + @Test + @Throws(IOException::class) + fun migrate10To11() { + helper.createDatabase(TEST_DB, 10).apply { + close() + } + helper.runMigrationsAndValidate(TEST_DB, 11, true, Migrations.MIGRATION_10_11) + } + + @Test + @Throws(IOException::class) + fun migrate11To12() { + helper.createDatabase(TEST_DB, 11).apply { + close() + } + helper.runMigrationsAndValidate(TEST_DB, 12, true, Migrations.MIGRATION_11_12) + } + + @Test + @Throws(IOException::class) + fun migrate12To13() { + helper.createDatabase(TEST_DB, 12).apply { + close() + } + helper.runMigrationsAndValidate(TEST_DB, 13, true, Migrations.MIGRATION_12_13) + } + + @Test + @Throws(IOException::class) + fun migrate13To14() { + helper.createDatabase(TEST_DB, 13).apply { + close() + } + helper.runMigrationsAndValidate(TEST_DB, 14, true, Migrations.MIGRATION_13_14) + } + + @Test + @Throws(IOException::class) + fun migrate14To15() { + helper.createDatabase(TEST_DB, 14).apply { + close() + } + helper.runMigrationsAndValidate(TEST_DB, 15, true, Migrations.MIGRATION_14_15) + } + + @Test + @Throws(IOException::class) + fun migrate15To16() { + helper.createDatabase(TEST_DB, 15).apply { + close() + } + helper.runMigrationsAndValidate(TEST_DB, 16, true, Migrations.MIGRATION_15_16) + } + + @Test + @Throws(IOException::class) + fun migrate17To19() { + helper.createDatabase(TEST_DB, 17).apply { + close() + } + helper.runMigrationsAndValidate(TEST_DB, 19, true, Migrations.MIGRATION_17_19) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java b/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java new file mode 100644 index 0000000..415f247 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java @@ -0,0 +1,132 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui; + +import android.os.Bundle; + +import com.nextcloud.talk.R; +import com.nextcloud.talk.activities.MainActivity; + +import org.junit.Ignore; +import org.junit.Test; + +import java.util.Objects; + +import androidx.test.core.app.ActivityScenario; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.espresso.web.webdriver.DriverAtoms; +import androidx.test.espresso.web.webdriver.Locator; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.typeText; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static androidx.test.espresso.web.sugar.Web.onWebView; +import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement; +import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick; +import static org.junit.Assert.assertEquals; + + +//@LargeTest +@Ignore("This test is ignored because it constantly fails on CI") +public class LoginIT { + + @Test + public void login() throws InterruptedException { + + ActivityScenario activityScenario = ActivityScenario.launch(MainActivity.class); + + Bundle arguments = androidx.test.platform.app.InstrumentationRegistry.getArguments(); + + String baseUrl = arguments.getString("TEST_SERVER_URL"); + String loginName = arguments.getString("TEST_SERVER_USERNAME"); + String password = arguments.getString("TEST_SERVER_PASSWORD"); + + Thread.sleep(2000); + + try { + onView(withId(R.id.serverEntryTextInputEditText)).check(matches(isDisplayed())); + } catch (NoMatchingViewException e) { + try { + // can happen that an invalid account from previous tests is existing + onView(withText(R.string.nc_settings_remove_account)).perform(click()); + Thread.sleep(2000); + } catch (NoMatchingViewException ie) { + // is OK if the dialog is not shown + } + + try { + // Delete account if exists + onView(withId(R.id.switch_account_button)).perform(click()); + onView(withId(R.id.manage_settings)).perform(click()); + onView(withId(R.id.settings_remove_account)).perform(click()); + onView(withText(R.string.nc_settings_remove)).perform(click()); + // The remove button must be clicked two times + onView(withId(R.id.settings_remove_account)).perform(click()); + // And yes: The button must be clicked two times + onView(withText(R.string.nc_settings_remove)).perform(click()); + onView(withText(R.string.nc_settings_remove)).perform(click()); + } catch (Exception ie2) { + // ignore + } finally { + Thread.sleep(2000); + } + + } + + onView(withId(R.id.serverEntryTextInputEditText)).perform(typeText(baseUrl)); + // Click on EditText's drawable right + onView(withContentDescription(R.string.nc_server_connect)).perform(click()); + + Thread.sleep(4000); + + onWebView().forceJavascriptEnabled(); + + // click on login + onWebView() + .withElement(findElement(Locator.XPATH, "//p[@id='redirect-link']/a")) + .perform(webClick()); + + // username + onWebView() + .withElement(findElement(Locator.XPATH, "//input[@id='user']")) + .perform(DriverAtoms.webKeys(loginName)); + + // password + onWebView() + .withElement(findElement(Locator.XPATH, "//input[@id='password']")) + .perform(DriverAtoms.webKeys(password)); + + // click login + onWebView() + .withElement(findElement(Locator.XPATH, "//input[@id='submit-form']")) + .perform(webClick()); + + Thread.sleep(2000); + + // grant access + onWebView() + .withElement(findElement(Locator.XPATH, "//input[@type='submit']")) + .perform(webClick()); + + Thread.sleep(5 * 1000); + + onView(withId(R.id.switch_account_button)).perform(click()); + onView(withId(R.id.user_name)).check(matches(withText("User One"))); + + activityScenario.onActivity(activity -> { + assertEquals(loginName, + Objects.requireNonNull(activity.currentUserProvider.getCurrentUser().blockingGet()).getUserId()); + }); + } +} diff --git a/app/src/androidTest/java/com/nextcloud/talk/utils/ColorGeneratorTest.kt b/app/src/androidTest/java/com/nextcloud/talk/utils/ColorGeneratorTest.kt new file mode 100644 index 0000000..97f6f4c --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/utils/ColorGeneratorTest.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.utils + +import android.graphics.Color +import org.junit.Assert +import org.junit.Test + +class ColorGeneratorTest { + + @Test + fun testUsernameToColor() { + usernameToColorHexHelper("", "#0082c9") + usernameToColorHexHelper(",", "#1e78c1") + usernameToColorHexHelper(".", "#c98879") + usernameToColorHexHelper("admin", "#d09e6d") + usernameToColorHexHelper("123e4567-e89b-12d3-a456-426614174000", "#bc5c91") + usernameToColorHexHelper("Akeel Robertson", "#9750a4") + usernameToColorHexHelper("Brayden Truong", "#d09e6d") + usernameToColorHexHelper("Daphne Roy", "#9750a4") + usernameToColorHexHelper("Ellena Wright Frederic Conway", "#c37285") + usernameToColorHexHelper("Gianluca Hills", "#d6b461") + usernameToColorHexHelper("Haseeb Stephens", "#d6b461") + usernameToColorHexHelper("Idris Mac", "#9750a4") + usernameToColorHexHelper("Kristi Fisher", "#0082c9") + usernameToColorHexHelper("Lillian Wall", "#bc5c91") + usernameToColorHexHelper("Lorelai Taylor", "#ddcb55") + usernameToColorHexHelper("Madina Knight", "#9750a4") + usernameToColorHexHelper("Meeting", "#c98879") + usernameToColorHexHelper("Private Circle", "#c37285") + usernameToColorHexHelper("Rae Hope", "#795aab") + usernameToColorHexHelper("Santiago Singleton", "#bc5c91") + usernameToColorHexHelper("Sid Combs", "#d09e6d") + usernameToColorHexHelper("TestCircle", "#499aa2") + usernameToColorHexHelper("Tom Mörtel", "#248eb5") + usernameToColorHexHelper("Vivienne Jacobs", "#1e78c1") + usernameToColorHexHelper("Zaki Cortes", "#6ea68f") + usernameToColorHexHelper("a user", "#5b64b3") + usernameToColorHexHelper("admin@cloud.example.com", "#9750a4") + usernameToColorHexHelper("another user", "#ddcb55") + usernameToColorHexHelper("asd", "#248eb5") + usernameToColorHexHelper("bar", "#0082c9") + usernameToColorHexHelper("foo", "#d09e6d") + usernameToColorHexHelper("wasd", "#b6469d") + usernameToColorHexHelper("مرحبا بالعالم", "#c98879") + usernameToColorHexHelper("🙈", "#b6469d") + } + + private fun usernameToColorHexHelper(username: String, expectedHexColor: String) { + val userColorInt = ColorGenerator.usernameToColor(username) // returns Int + val userHexColor = intToHex(userColorInt) + + Assert.assertEquals(expectedHexColor.lowercase(), userHexColor.lowercase()) + } + + private fun intToHex(colorInt: Int): String { + val r = Color.red(colorInt) + val g = Color.green(colorInt) + val b = Color.blue(colorInt) + return String.format("#%02x%02x%02x", r, g, b) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/talk/utils/ShareUtilsIT.kt b/app/src/androidTest/java/com/nextcloud/talk/utils/ShareUtilsIT.kt new file mode 100644 index 0000000..ea1ab84 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/utils/ShareUtilsIT.kt @@ -0,0 +1,55 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import at.bitfire.dav4jvm.HttpUtils +import org.apache.commons.lang3.time.DateUtils +import org.junit.Assert.assertEquals +import org.junit.Ignore +import org.junit.Test +import java.util.Date +import java.util.Locale + +@Ignore("Test fails on CI server. See issue https://github.com/nextcloud/talk-android/issues/1737") +class ShareUtilsIT { + @Test + fun date() { + assertEquals(TEST_DATE_IN_MILLIS, parseDate2("Mon, 09 Apr 2008 23:55:38 GMT").time) + assertEquals(TEST_DATE_IN_MILLIS, HttpUtils.parseDate("Mon, 09 Apr 2008 23:55:38 GMT")?.time) + } + + private fun parseDate2(dateStr: String): Date = + DateUtils.parseDate( + dateStr, Locale.US, + HttpUtils.httpDateFormatStr, + // RFC 822, updated by RFC 1123 with any TZ + "EEE, dd MMM yyyy HH:mm:ss zzz", + // RFC 850, obsoleted by RFC 1036 with any TZ. + "EEEE, dd-MMM-yy HH:mm:ss zzz", + // ANSI C's asctime() format + "EEE MMM d HH:mm:ss yyyy", + // Alternative formats. + "EEE, dd-MMM-yyyy HH:mm:ss z", + "EEE, dd-MMM-yyyy HH-mm-ss z", + "EEE, dd MMM yy HH:mm:ss z", + "EEE dd-MMM-yyyy HH:mm:ss z", + "EEE dd MMM yyyy HH:mm:ss z", + "EEE dd-MMM-yyyy HH-mm-ss z", + "EEE dd-MMM-yy HH:mm:ss z", + "EEE dd MMM yy HH:mm:ss z", + "EEE,dd-MMM-yy HH:mm:ss z", + "EEE,dd-MMM-yyyy HH:mm:ss z", + "EEE, dd-MM-yyyy HH:mm:ss z", + // RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com + "EEE MMM d yyyy HH:mm:ss z" + ) + + companion object { + private const val TEST_DATE_IN_MILLIS = 1207778138000 + } +} diff --git a/app/src/androidTest/java/com/nextcloud/talk/utils/UriUtilsIT.kt b/app/src/androidTest/java/com/nextcloud/talk/utils/UriUtilsIT.kt new file mode 100644 index 0000000..2a23e61 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/utils/UriUtilsIT.kt @@ -0,0 +1,142 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Samanwith KSN + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import org.junit.Test + +class UriUtilsIT { + + @Test + fun testHasHttpProtocolPrefixed() { + val uriHttp = "http://www.example.com" + val resultHttp = UriUtils.hasHttpProtocolPrefixed(uriHttp) + assertTrue(resultHttp) + + val uriHttps = "https://www.example.com" + val resultHttps = UriUtils.hasHttpProtocolPrefixed(uriHttps) + assertTrue(resultHttps) + + val uriWithoutPrefix = "www.example.com" + val resultWithoutPrefix = UriUtils.hasHttpProtocolPrefixed(uriWithoutPrefix) + assertFalse(resultWithoutPrefix) + } + + @Test + fun testExtractInstanceInternalFileFileId() { + assertEquals( + "42", + UriUtils.extractInstanceInternalFileFileId( + "https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=42" + ) + ) + } + + @Test + fun testExtractInstanceInternalFileShareFileId() { + assertEquals( + "42", + UriUtils.extractInstanceInternalFileShareFileId("https://cloud.nextcloud.com/f/42") + ) + } + + @Test + fun testIsInstanceInternalFileShareUrl() { + assertTrue( + UriUtils.isInstanceInternalFileShareUrl( + "https://cloud.nextcloud.com", + "https://cloud.nextcloud.com/f/42" + ) + ) + + assertFalse( + UriUtils.isInstanceInternalFileShareUrl( + "https://nextcloud.nextcloud.com", + "https://cloud.nextcloud.com/f/42" + ) + ) + + assertFalse( + UriUtils.isInstanceInternalFileShareUrl( + "https://nextcloud.nextcloud.com", + "https://cloud.nextcloud.com/f/" + ) + ) + + assertFalse( + UriUtils.isInstanceInternalFileShareUrl( + "https://nextcloud.nextcloud.com", + "https://cloud.nextcloud.com/f/test123" + ) + ) + } + + @Test + fun testIsInstanceInternalFileUrl() { + assertTrue( + UriUtils.isInstanceInternalFileUrl( + "https://cloud.nextcloud.com", + "https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41" + ) + ) + + assertFalse( + UriUtils.isInstanceInternalFileUrl( + "https://nextcloud.nextcloud.com", + "https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41" + ) + ) + + assertFalse( + UriUtils.isInstanceInternalFileUrl( + "https://nextcloud.nextcloud.com", + "https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=test123" + ) + ) + + assertFalse( + UriUtils.isInstanceInternalFileUrl( + "https://nextcloud.nextcloud.com", + "https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=" + ) + ) + + assertFalse( + UriUtils.isInstanceInternalFileUrl( + "https://cloud.nextcloud.com", + "https://cloud.nextcloud.com/apps/files/?dir=/Engineering" + ) + ) + } + + @Test + fun testIsInstanceInternalFileUrlNew() { + assertTrue( + UriUtils.isInstanceInternalFileUrlNew( + "https://cloud.nextcloud.com", + "https://cloud.nextcloud.com/apps/files/files/41?dir=/" + ) + ) + + assertFalse( + UriUtils.isInstanceInternalFileUrlNew( + "https://nextcloud.nextcloud.com", + "https://cloud.nextcloud.com/apps/files/files/41?dir=/" + ) + ) + } + + @Test + fun testExtractInstanceInternalFileFileIdNew() { + assertEquals( + "42", + UriUtils.extractInstanceInternalFileFileIdNew("https://cloud.nextcloud.com/apps/files/files/42?dir=/") + ) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/talk/utils/VibrationUtilsTest.kt b/app/src/androidTest/java/com/nextcloud/talk/utils/VibrationUtilsTest.kt new file mode 100644 index 0000000..a0933cb --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/talk/utils/VibrationUtilsTest.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Samanwith KSN + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.Context +import android.os.VibrationEffect +import android.os.Vibrator +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class VibrationUtilsTest { + + @Mock + private lateinit var mockContext: Context + + @Mock + private lateinit var mockVibrator: Vibrator + + @Before + fun setup() { + Mockito.`when`(mockContext.getSystemService(Context.VIBRATOR_SERVICE)).thenReturn(mockVibrator) + } + + @Test + fun testVibrateShort() { + VibrationUtils.vibrateShort(mockContext) + Mockito.verify(mockVibrator) + .vibrate( + VibrationEffect + .createOneShot( + VibrationUtils.SHORT_VIBRATE, + VibrationEffect.DEFAULT_AMPLITUDE + ) + ) + } +} diff --git a/app/src/generic/fastlane/metadata/android/tr-TR/full_description.txt b/app/src/generic/fastlane/metadata/android/tr-TR/full_description.txt new file mode 100644 index 0000000..968abf7 --- /dev/null +++ b/app/src/generic/fastlane/metadata/android/tr-TR/full_description.txt @@ -0,0 +1,27 @@ +Birebir ya da grup ile ses ya da görüntü çağrıları yapmak, web konferansları oluşturmak ve sohbet iletileri göndermek için Nextcloud Talk uygulamasını kullanabilisiniz. Tüm bilgiler tamamen şifrelenmiş olarak sizin sunucunuz üzerinden iletilerek olabilecek en yüksek gizlilik düzeyi sağlanır. + +Nextcloud Talk uygulamasının kullanımı kolaydır ve herhangi bir ücret ödemeniz gerekmez! + +Nextcloud Talk şu özellikleri destekler: +* HD (H.265) ses ve görüntü çağrıları +* Grup ve birebir çağrılar +* Webinar ve herkese açık web toplantıları +* Bireysel ve grup sohbetleri +* Kolay ekran paylaşımı +* Android ve iOS uygulamaları +* Mobil çağrı ve anında sohbet bildirimleri +* Nextcloud Files ve Nextcloud Groupware ile bütünleşme +* Kendi veri merkezinizde, %100 açık kaynaklı +* Uçtan uca şifrelenmiş çağrılar +* Milyonlarca kullanıcıya göre +* SIP geçidi üzerinden telefon ile arama + +Nextcloud Talk uygulamasının kullanılabilmesi için Nextcloud Talk sunucusu gereklidir. Nextcloud verilerinizin kontrolunu yeniden size veren, size özel, kendi sunucularınızda barındırabileceğiniz bir dosya eşitleme ve iletişim platformudur. Evinizde, hizmet sağlayıcınızda ya da kurumunuzda bulunan bir sunucu üzerinde kullanarak, belgelerinize, takvimlerinize, kişilerinize, e-postalarınıza ve diğer verilerinize erişmenizi sağlar. Verilerinizi farklı Nextcloud sunucuları kullanan kişiler ile paylaşarak dosyalar üzerinde birlikte çalışabilirsiniz. Nextcloud tamamen açık kaynaklı olduğundan özelliklerini gereksinimlerinize göre genişletebilir, geliştirilmesine katkıda bulunabilir ya da vaatlerimizin doğruluğunu görebilirsiniz. + + +Dünya üzerinde milyonlarca kullanıcı ev ya da iş yerlerinde günlük işlemleri için Nextcloud kullanıyor. Kurumsal kullanıcılar Nextcloud GmbH tarafından sunulan destek hizmetleri ile tamamen kendi BT bölümlerinin denetimindeki kurumsal üretim ve iş birliği platformu için eksiksiz destek alıyorlar. + + +Ayrıntılı bilgi almak için https://nextcloud.com/talk + +Nextcloud web sitesi https://nextcloud.com diff --git a/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java b/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java new file mode 100644 index 0000000..cafaa07 --- /dev/null +++ b/app/src/generic/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils; + + +import com.nextcloud.talk.interfaces.ClosedInterface; + +public class ClosedInterfaceImpl implements ClosedInterface { + @Override + public void providerInstallerInstallIfNeededAsync() { + // does absolutely nothing :) + } + + @Override + public boolean isGooglePlayServicesAvailable() { + return false; + } + + @Override + public void setUpPushTokenRegistration() { + // no push notifications for generic build variant + } +} diff --git a/app/src/gplay/AndroidManifest.xml b/app/src/gplay/AndroidManifest.xml new file mode 100644 index 0000000..ee962f6 --- /dev/null +++ b/app/src/gplay/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt b/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt new file mode 100644 index 0000000..964795a --- /dev/null +++ b/app/src/gplay/java/com/nextcloud/talk/jobs/GetFirebasePushTokenWorker.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import autodagger.AutoInjector +import com.google.android.gms.tasks.OnCompleteListener +import com.google.firebase.messaging.FirebaseMessaging +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.utils.preferences.AppPreferences +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class GetFirebasePushTokenWorker(val context: Context, workerParameters: WorkerParameters) : + Worker(context, workerParameters) { + + @Inject + lateinit var appPreferences: AppPreferences + + @SuppressLint("LongLogTag") + override fun doWork(): Result { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + FirebaseMessaging.getInstance().token.addOnCompleteListener( + OnCompleteListener { task -> + if (!task.isSuccessful) { + Log.w(TAG, "Fetching FCM registration token failed", task.exception) + return@OnCompleteListener + } + + val pushToken = task.result + Log.d(TAG, "Fetched firebase push token is: $pushToken") + + appPreferences.pushToken = pushToken + appPreferences.pushTokenLatestFetch = System.currentTimeMillis() + + val data: Data = + Data.Builder().putString(PushRegistrationWorker.ORIGIN, "GetFirebasePushTokenWorker").build() + val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java) + .setInputData(data) + .build() + WorkManager.getInstance(context).enqueue(pushRegistrationWork) + } + ) + + return Result.success() + } + + companion object { + private val TAG = GetFirebasePushTokenWorker::class.simpleName + } +} diff --git a/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt new file mode 100644 index 0000000..ac4e888 --- /dev/null +++ b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt @@ -0,0 +1,81 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.services.firebase + +import android.util.Log +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.jobs.NotificationWorker +import com.nextcloud.talk.jobs.PushRegistrationWorker +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.preferences.AppPreferences +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class NCFirebaseMessagingService : FirebaseMessagingService() { + + @Inject + lateinit var appPreferences: AppPreferences + + override fun onCreate() { + Log.d(TAG, "onCreate") + super.onCreate() + sharedApplication!!.componentApplication.inject(this) + } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + Log.d(TAG, "onMessageReceived") + sharedApplication!!.componentApplication.inject(this) + + Log.d(TAG, "remoteMessage.priority: " + remoteMessage.priority) + Log.d(TAG, "remoteMessage.originalPriority: " + remoteMessage.originalPriority) + + val data = remoteMessage.data + val subject = data[KEY_NOTIFICATION_SUBJECT] + val signature = data[KEY_NOTIFICATION_SIGNATURE] + + if (!subject.isNullOrEmpty() && !signature.isNullOrEmpty()) { + val messageData = Data.Builder() + .putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject) + .putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature) + .build() + val notificationWork = + OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData) + .build() + WorkManager.getInstance().enqueue(notificationWork) + } + } + + override fun onNewToken(token: String) { + super.onNewToken(token) + Log.d(TAG, "onNewToken. token = $token") + + appPreferences.pushToken = token + appPreferences.pushTokenLatestGeneration = System.currentTimeMillis() + + val data: Data = + Data.Builder().putString(PushRegistrationWorker.ORIGIN, "NCFirebaseMessagingService#onNewToken").build() + val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java) + .setInputData(data) + .build() + WorkManager.getInstance().enqueue(pushRegistrationWork) + } + + companion object { + private val TAG = NCFirebaseMessagingService::class.simpleName + const val KEY_NOTIFICATION_SUBJECT = "subject" + const val KEY_NOTIFICATION_SIGNATURE = "signature" + } +} diff --git a/app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt b/app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt new file mode 100644 index 0000000..b16da48 --- /dev/null +++ b/app/src/gplay/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt @@ -0,0 +1,93 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.Intent +import android.util.Log +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.security.ProviderInstaller +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.interfaces.ClosedInterface +import com.nextcloud.talk.jobs.GetFirebasePushTokenWorker +import java.util.concurrent.TimeUnit + +@AutoInjector(NextcloudTalkApplication::class) +class ClosedInterfaceImpl : + ClosedInterface, + ProviderInstaller.ProviderInstallListener { + + override val isGooglePlayServicesAvailable: Boolean = isGPlayServicesAvailable() + + override fun providerInstallerInstallIfNeededAsync() { + NextcloudTalkApplication.sharedApplication?.let { + ProviderInstaller.installIfNeededAsync( + it.applicationContext, + this + ) + } + } + + override fun onProviderInstalled() { + // unused atm + } + + override fun onProviderInstallFailed(p0: Int, p1: Intent?) { + // unused atm + } + + private fun isGPlayServicesAvailable(): Boolean { + val api = GoogleApiAvailability.getInstance() + val code = + NextcloudTalkApplication.sharedApplication?.let { + api.isGooglePlayServicesAvailable(it.applicationContext) + } + return if (code == ConnectionResult.SUCCESS) { + true + } else { + Log.w(TAG, "GooglePlayServices are not available. Code:$code") + false + } + } + + override fun setUpPushTokenRegistration() { + val firebasePushTokenWorker = OneTimeWorkRequest.Builder(GetFirebasePushTokenWorker::class.java).build() + WorkManager.getInstance().enqueue(firebasePushTokenWorker) + + setUpPeriodicTokenRefreshFromFCM() + } + + private fun setUpPeriodicTokenRefreshFromFCM() { + val periodicTokenRefreshFromFCM = PeriodicWorkRequest.Builder( + GetFirebasePushTokenWorker::class.java, + DAILY, + TimeUnit.HOURS, + FLEX_INTERVAL, + TimeUnit.HOURS + ).build() + + WorkManager.getInstance() + .enqueueUniquePeriodicWork( + "periodicTokenRefreshFromFCM", + ExistingPeriodicWorkPolicy.UPDATE, + periodicTokenRefreshFromFCM + ) + } + + companion object { + private val TAG = ClosedInterfaceImpl::class.java.simpleName + const val DAILY: Long = 24 + const val FLEX_INTERVAL: Long = 10 + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..384b7c1 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/leafletMapMessagePreview.html b/app/src/main/assets/leafletMapMessagePreview.html new file mode 100644 index 0000000..b1a8197 --- /dev/null +++ b/app/src/main/assets/leafletMapMessagePreview.html @@ -0,0 +1,42 @@ + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/app/src/main/assets/leafletMapMessagePreview.html.license b/app/src/main/assets/leafletMapMessagePreview.html.license new file mode 100644 index 0000000..fb33390 --- /dev/null +++ b/app/src/main/assets/leafletMapMessagePreview.html.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2021 Marcel Hibbe +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..70df96e9076516d830c4e2bd962d1b73cf070e4d GIT binary patch literal 25403 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 A(*OVf literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/nextcloud/talk/PhoneUtils.kt b/app/src/main/java/com/nextcloud/talk/PhoneUtils.kt new file mode 100644 index 0000000..4397574 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/PhoneUtils.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk + +object PhoneUtils { + fun isPhoneNumber(input: String?): Boolean = input?.matches(Regex("^\\+?\\d+$")) == true +} diff --git a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt new file mode 100644 index 0000000..d327973 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt @@ -0,0 +1,521 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.account + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.os.Handler +import android.text.TextUtils +import android.util.Log +import android.widget.Toast +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.bluelinelabs.logansquare.LoganSquare +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.conversationlist.ConversationsListActivity +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityAccountVerificationBinding +import com.nextcloud.talk.events.EventStatus +import com.nextcloud.talk.jobs.AccountRemovalWorker +import com.nextcloud.talk.jobs.CapabilitiesWorker +import com.nextcloud.talk.jobs.SignalingSettingsWorker +import com.nextcloud.talk.jobs.WebsocketConnectionsWorker +import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall +import com.nextcloud.talk.models.json.generic.Status +import com.nextcloud.talk.models.json.userprofile.UserProfileOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ClosedInterfaceImpl +import com.nextcloud.talk.utils.UriUtils +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PASSWORD +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME +import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder +import io.reactivex.MaybeObserver +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.net.CookieManager +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class AccountVerificationActivity : BaseActivity() { + + private lateinit var binding: ActivityAccountVerificationBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var cookieManager: CookieManager + + private var internalAccountId: Long = -1 + private val disposables: MutableList = ArrayList() + private var baseUrl: String? = null + private var username: String? = null + private var token: String? = null + private var isAccountImport = false + private var originalProtocol: String? = null + + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + binding = ActivityAccountVerificationBinding.inflate(layoutInflater) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + setContentView(binding.root) + actionBar?.hide() + initSystemBars() + + handleIntent() + } + + private fun handleIntent() { + val extras = intent.extras!! + baseUrl = extras.getString(KEY_BASE_URL) + username = extras.getString(KEY_USERNAME) + token = extras.getString(KEY_TOKEN) + if (extras.containsKey(KEY_IS_ACCOUNT_IMPORT)) { + isAccountImport = true + } + if (extras.containsKey(KEY_ORIGINAL_PROTOCOL)) { + originalProtocol = extras.getString(KEY_ORIGINAL_PROTOCOL) + } + } + + override fun onResume() { + super.onResume() + + if ( + isAccountImport && + !UriUtils.hasHttpProtocolPrefixed(baseUrl!!) || + isNotSameProtocol(baseUrl!!, originalProtocol) + ) { + determineBaseUrlProtocol(true) + } else { + findServerTalkApp() + } + } + + private fun isNotSameProtocol(baseUrl: String, originalProtocol: String?): Boolean { + if (originalProtocol == null) { + return true + } + return !TextUtils.isEmpty(originalProtocol) && !baseUrl.startsWith(originalProtocol) + } + + private fun determineBaseUrlProtocol(checkForcedHttps: Boolean) { + cookieManager.cookieStore.removeAll() + baseUrl = baseUrl!!.replace("http://", "").replace("https://", "") + val queryUrl: String = if (checkForcedHttps) { + "https://" + baseUrl + ApiUtils.getUrlPostfixForStatus() + } else { + "http://" + baseUrl + ApiUtils.getUrlPostfixForStatus() + } + ncApi.getServerStatus(queryUrl) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + + override fun onNext(status: Status) { + baseUrl = if (checkForcedHttps) { + "https://$baseUrl" + } else { + "http://$baseUrl" + } + if (isAccountImport) { + val bundle = Bundle() + bundle.putString(KEY_BASE_URL, baseUrl) + bundle.putString(KEY_USERNAME, username) + bundle.putString(KEY_PASSWORD, "") + + val intent = Intent(context, WebViewLoginActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } else { + findServerTalkApp() + } + } + + override fun onError(e: Throwable) { + if (checkForcedHttps) { + determineBaseUrlProtocol(false) + } else { + abortVerification() + } + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun findServerTalkApp() { + val credentials = ApiUtils.getCredentials(username, token) + cookieManager.cookieStore.removeAll() + + ncApi.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl!!)) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + + override fun onNext(capabilitiesOverall: CapabilitiesOverall) { + val hasTalk = + capabilitiesOverall.ocs!!.data!!.capabilities != null && + capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability != null && + capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability!!.features != null && + !capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability!!.features!!.isEmpty() + if (hasTalk) { + fetchProfile(credentials!!, capabilitiesOverall) + } else { + if (resources != null) { + runOnUiThread { + binding.progressText.text = String.format( + resources!!.getString(R.string.nc_nextcloud_talk_app_not_installed), + resources!!.getString(R.string.nc_app_product_name) + ) + } + } + ApplicationWideMessageHolder.getInstance().messageType = + ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK + abortVerification() + } + } + + override fun onError(e: Throwable) { + if (resources != null) { + runOnUiThread { + binding.progressText.text = String.format( + resources!!.getString(R.string.nc_nextcloud_talk_app_not_installed), + resources!!.getString(R.string.nc_app_product_name) + ) + } + } + ApplicationWideMessageHolder.getInstance().messageType = + ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK + abortVerification() + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun storeProfile(displayName: String?, userId: String, capabilitiesOverall: CapabilitiesOverall) { + userManager.storeProfile( + username, + UserManager.UserAttributes( + id = null, + serverUrl = baseUrl, + currentUser = true, + userId = userId, + token = token, + displayName = displayName, + pushConfigurationState = null, + capabilities = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.capabilities), + serverVersion = LoganSquare.serialize(capabilitiesOverall.ocs!!.data!!.serverVersion), + certificateAlias = appPreferences.temporaryClientCertAlias, + externalSignalingServer = null + ) + ) + .subscribeOn(Schedulers.io()) + .subscribe(object : MaybeObserver { + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + + @SuppressLint("SetTextI18n") + override fun onSuccess(user: User) { + internalAccountId = user.id!! + if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) { + ClosedInterfaceImpl().setUpPushTokenRegistration() + } else { + Log.w(TAG, "Skipping push registration.") + runOnUiThread { + binding.progressText.text = + """ ${binding.progressText.text} + ${resources!!.getString(R.string.nc_push_disabled)} + """.trimIndent() + } + fetchAndStoreCapabilities() + } + } + + @SuppressLint("SetTextI18n") + override fun onError(e: Throwable) { + binding.progressText.text = """ ${binding.progressText.text}""".trimIndent() + + resources!!.getString(R.string.nc_display_name_not_stored) + abortVerification() + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun fetchProfile(credentials: String, capabilitiesOverall: CapabilitiesOverall) { + ncApi.getUserProfile( + credentials, + ApiUtils.getUrlForUserProfile(baseUrl!!) + ) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + + @SuppressLint("SetTextI18n") + override fun onNext(userProfileOverall: UserProfileOverall) { + var displayName: String? = null + if (!TextUtils.isEmpty(userProfileOverall.ocs!!.data!!.displayName)) { + displayName = userProfileOverall.ocs!!.data!!.displayName + } else if (!TextUtils.isEmpty(userProfileOverall.ocs!!.data!!.displayNameAlt)) { + displayName = userProfileOverall.ocs!!.data!!.displayNameAlt + } + if (!TextUtils.isEmpty(displayName)) { + storeProfile( + displayName, + userProfileOverall.ocs!!.data!!.userId!!, + capabilitiesOverall + ) + } else { + runOnUiThread { + binding.progressText.text = + """ + ${binding.progressText.text} + ${resources!!.getString(R.string.nc_display_name_not_fetched)} + """.trimIndent() + } + abortVerification() + } + } + + @SuppressLint("SetTextI18n") + override fun onError(e: Throwable) { + runOnUiThread { + binding.progressText.text = + """ + ${binding.progressText.text} + ${resources!!.getString(R.string.nc_display_name_not_fetched)} + """.trimIndent() + } + abortVerification() + } + + override fun onComplete() { + // unused atm + } + }) + } + + @SuppressLint("SetTextI18n") + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(eventStatus: EventStatus) { + Log.d(TAG, "caught EventStatus of type " + eventStatus.eventType.toString()) + if (eventStatus.eventType == EventStatus.EventType.PUSH_REGISTRATION) { + if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) { + runOnUiThread { + binding.progressText.text = + """ + ${binding.progressText.text} + ${resources!!.getString(R.string.nc_push_disabled)} + """.trimIndent() + } + } + fetchAndStoreCapabilities() + } else if (eventStatus.eventType == EventStatus.EventType.CAPABILITIES_FETCH) { + if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) { + runOnUiThread { + binding.progressText.text = + """ + ${binding.progressText.text} + ${resources!!.getString(R.string.nc_capabilities_failed)} + """.trimIndent() + } + abortVerification() + } else if (internalAccountId == eventStatus.userId && eventStatus.isAllGood) { + fetchAndStoreExternalSignalingSettings() + } + } else if (eventStatus.eventType == EventStatus.EventType.SIGNALING_SETTINGS) { + if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) { + runOnUiThread { + binding.progressText.text = + """ + ${binding.progressText.text} + ${resources!!.getString(R.string.nc_external_server_failed)} + """.trimIndent() + } + } + proceedWithLogin() + } + } + + private fun fetchAndStoreCapabilities() { + val userData = + Data.Builder() + .putLong(KEY_INTERNAL_USER_ID, internalAccountId) + .build() + val capabilitiesWork = + OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java) + .setInputData(userData) + .build() + WorkManager.getInstance().enqueue(capabilitiesWork) + } + + private fun fetchAndStoreExternalSignalingSettings() { + val userData = + Data.Builder() + .putLong(KEY_INTERNAL_USER_ID, internalAccountId) + .build() + val signalingSettingsWorker = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java) + .setInputData(userData) + .build() + val websocketConnectionsWorker = OneTimeWorkRequest.Builder(WebsocketConnectionsWorker::class.java).build() + + WorkManager.getInstance(applicationContext!!) + .beginWith(signalingSettingsWorker) + .then(websocketConnectionsWorker) + .enqueue() + } + + private fun proceedWithLogin() { + cookieManager.cookieStore.removeAll() + + if (userManager.users.blockingGet().size == 1 || + currentUserProvider.currentUser.blockingGet().id != internalAccountId + ) { + val userToSetAsActive = userManager.getUserWithId(internalAccountId).blockingGet() + Log.d(TAG, "userToSetAsActive: " + userToSetAsActive.username) + + if (userManager.setUserAsActive(userToSetAsActive).blockingGet()) { + runOnUiThread { + if (userManager.users.blockingGet().size == 1) { + val intent = Intent(context, ConversationsListActivity::class.java) + startActivity(intent) + } else { + if (isAccountImport) { + ApplicationWideMessageHolder.getInstance().messageType = + ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED + } + val intent = Intent(context, ConversationsListActivity::class.java) + startActivity(intent) + } + } + } else { + Log.e(TAG, "failed to set active user") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + } else { + Log.d(TAG, "continuing proceedWithLogin was skipped for this user") + } + } + + private fun dispose() { + for (i in disposables.indices) { + if (!disposables[i].isDisposed) { + disposables[i].dispose() + } + } + } + + public override fun onDestroy() { + dispose() + super.onDestroy() + } + + private fun abortVerification() { + if (isAccountImport) { + ApplicationWideMessageHolder.getInstance().messageType = ApplicationWideMessageHolder.MessageType + .FAILED_TO_IMPORT_ACCOUNT + runOnUiThread { + Handler().postDelayed({ + val intent = Intent(this, ServerSelectionActivity::class.java) + startActivity(intent) + }, DELAY_IN_MILLIS) + } + } else { + if (internalAccountId != -1L) { + runOnUiThread { + deleteUserAndStartServerSelection(internalAccountId) + } + } else { + runOnUiThread { + Handler().postDelayed({ + val intent = Intent(this, ServerSelectionActivity::class.java) + startActivity(intent) + }, DELAY_IN_MILLIS) + } + } + } + } + + @SuppressLint("CheckResult") + private fun deleteUserAndStartServerSelection(userId: Long) { + userManager.scheduleUserForDeletionWithId(userId).blockingGet() + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo? -> + + when (workInfo?.state) { + WorkInfo.State.SUCCEEDED -> { + val intent = Intent(this, ServerSelectionActivity::class.java) + startActivity(intent) + } + + WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "something went wrong when deleting user with id $userId") + val intent = Intent(this, ServerSelectionActivity::class.java) + startActivity(intent) + } + + else -> {} + } + } + } + + companion object { + private val TAG = AccountVerificationActivity::class.java.simpleName + const val DELAY_IN_MILLIS: Long = 7500 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt new file mode 100644 index 0000000..d3a12a0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt @@ -0,0 +1,468 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.account + +import android.Manifest +import android.accounts.Account +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.security.KeyChain +import android.text.TextUtils +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.TextView +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.net.toUri +import androidx.core.os.bundleOf +import autodagger.AutoInjector +import com.blikoon.qrcodescanner.QrCodeActivity +import com.github.dhaval2404.imagepicker.util.PermissionUtil +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.databinding.ActivityServerSelectionBinding +import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall +import com.nextcloud.talk.models.json.generic.Status +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.AccountUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.UriUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ADDITIONAL_ACCOUNT +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT +import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import java.security.cert.CertificateException +import javax.inject.Inject + +@Suppress("TooManyFunctions") +@AutoInjector(NextcloudTalkApplication::class) +class ServerSelectionActivity : BaseActivity() { + + private lateinit var binding: ActivityServerSelectionBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + private var statusQueryDisposable: Disposable? = null + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (intent.hasExtra(ADD_ADDITIONAL_ACCOUNT) && intent.getBooleanExtra(ADD_ADDITIONAL_ACCOUNT, false)) { + finish() + } else { + finishAffinity() + } + } + } + + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + binding = ActivityServerSelectionBinding.inflate(layoutInflater) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + setContentView(binding.root) + actionBar?.hide() + initSystemBars() + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + override fun onResume() { + super.onResume() + + binding.hostUrlInputHelperText.text = String.format( + resources!!.getString(R.string.nc_server_helper_text), + resources!!.getString(R.string.nc_server_product_name) + ) + binding.serverEntryTextInputLayout.setEndIconOnClickListener { checkServerAndProceed() } + + if (resources!!.getBoolean(R.bool.hide_auth_cert)) { + binding.certTextView.visibility = View.GONE + } + + val loggedInUsers = userManager.users.blockingGet() + val availableAccounts = AccountUtils.findAvailableAccountsOnDevice(loggedInUsers) + + if (isImportAccountNameSet() && availableAccounts.isNotEmpty()) { + showImportAccountsInfo(availableAccounts) + } else if (isAbleToShowProviderLink() && loggedInUsers.isEmpty()) { + showVisitProvidersInfo() + } else { + binding.importOrChooseProviderText.visibility = View.INVISIBLE + } + + binding.serverEntryTextInputEditText.requestFocus() + if (!TextUtils.isEmpty(resources!!.getString(R.string.weblogin_url))) { + binding.serverEntryTextInputEditText.setText(resources!!.getString(R.string.weblogin_url)) + checkServerAndProceed() + } + binding.serverEntryTextInputEditText.setOnEditorActionListener { _: TextView?, i: Int, _: KeyEvent? -> + if (i == EditorInfo.IME_ACTION_DONE) { + checkServerAndProceed() + } + false + } + binding.certTextView.setOnClickListener { onCertClick() } + + binding.scanQr.setOnClickListener { onScan() } + + if (ApplicationWideMessageHolder.getInstance().messageType != null) { + if (ApplicationWideMessageHolder.getInstance().messageType + == ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK + ) { + setErrorText(resources!!.getString(R.string.nc_settings_no_talk_installed)) + } else if (ApplicationWideMessageHolder.getInstance().messageType + == ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT + ) { + setErrorText(resources!!.getString(R.string.nc_server_failed_to_import_account)) + } + ApplicationWideMessageHolder.getInstance().messageType = null + } + setCertTextView() + } + + fun onCertClick() { + KeyChain.choosePrivateKeyAlias( + this, + { alias: String? -> + if (alias != null) { + appPreferences.temporaryClientCertAlias = alias + } else { + appPreferences.removeTemporaryClientCertAlias() + } + setCertTextView() + }, + arrayOf("RSA", "EC"), + null, + null, + -1, + null + ) + } + + private fun isAbleToShowProviderLink(): Boolean = + !resources!!.getBoolean(R.bool.hide_provider) && + !TextUtils.isEmpty(resources!!.getString(R.string.nc_providers_url)) + + private fun showImportAccountsInfo(availableAccounts: List) { + if (!TextUtils.isEmpty( + AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from)) + ) + ) { + if (availableAccounts.size > 1) { + binding.importOrChooseProviderText.text = String.format( + resources!!.getString(R.string.nc_server_import_accounts), + AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from)) + ) + } else { + binding.importOrChooseProviderText.text = String.format( + resources!!.getString(R.string.nc_server_import_account), + AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from)) + ) + } + } else { + if (availableAccounts.size > 1) { + binding.importOrChooseProviderText.text = + resources!!.getString(R.string.nc_server_import_accounts_plain) + } else { + binding.importOrChooseProviderText.text = + resources!!.getString(R.string.nc_server_import_account_plain) + } + } + binding.importOrChooseProviderText.setOnClickListener { + val bundle = Bundle() + bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true) + val intent = Intent(context, SwitchAccountActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } + } + + private fun showVisitProvidersInfo() { + binding.importOrChooseProviderText.setText(R.string.nc_get_from_provider) + binding.importOrChooseProviderText.setOnClickListener { + val browserIntent = Intent( + Intent.ACTION_VIEW, + resources!!.getString(R.string.nc_providers_url).toUri() + ) + startActivity(browserIntent) + } + } + + private fun isImportAccountNameSet(): Boolean = + !TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type)) + + @SuppressLint("LongLogTag") + @Suppress("Detekt.TooGenericExceptionCaught") + private fun checkServerAndProceed() { + dispose() + var url: String = binding.serverEntryTextInputEditText.text.toString().trim() + showserverEntryProgressBar() + if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) { + binding.importOrChooseProviderText.visibility = View.INVISIBLE + binding.certTextView.visibility = View.INVISIBLE + } + if (url.endsWith("/")) { + url = url.substring(0, url.length - 1) + } + + if (UriUtils.hasHttpProtocolPrefixed(url)) { + checkServer(url, false) + } else { + checkServer("https://$url", true) + } + } + + private fun checkServer(url: String, checkForcedHttps: Boolean) { + val queryStatusUrl = url + ApiUtils.getUrlPostfixForStatus() + + statusQueryDisposable = ncApi.getServerStatus(queryStatusUrl) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ status: Status -> + val versionString: String = status.version!!.substring(0, status.version!!.indexOf(".")) + val version: Int = versionString.toInt() + + if (isServerStatusQueryable(status) && version >= MIN_SERVER_MAJOR_VERSION) { + findServerTalkApp(url) + } else { + showErrorTextForStatus(status) + } + }, { throwable: Throwable -> + if (checkForcedHttps) { + checkServer(queryStatusUrl.replace("https://", "http://"), false) + } else { + if (throwable.localizedMessage != null) { + setErrorText(throwable.localizedMessage) + } else if (throwable.cause is CertificateException) { + setErrorText(resources!!.getString(R.string.nc_certificate_error)) + } else { + hideserverEntryProgressBar() + } + + if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) { + binding.importOrChooseProviderText.visibility = View.VISIBLE + binding.certTextView.visibility = View.VISIBLE + } + dispose() + } + }) { + hideserverEntryProgressBar() + if (binding.importOrChooseProviderText.visibility != View.INVISIBLE) { + binding.importOrChooseProviderText.visibility = View.VISIBLE + binding.certTextView.visibility = View.VISIBLE + } + dispose() + } + } + + private fun showErrorTextForStatus(status: Status) { + if (!status.installed) { + setErrorText( + String.format( + resources!!.getString(R.string.nc_server_not_installed), + resources!!.getString(R.string.nc_server_product_name) + ) + ) + } else if (status.needsUpgrade) { + setErrorText( + String.format( + resources!!.getString(R.string.nc_server_db_upgrade_needed), + resources!!.getString(R.string.nc_server_product_name) + ) + ) + } else if (status.maintenance) { + setErrorText( + String.format( + resources!!.getString(R.string.nc_server_maintenance), + resources!!.getString(R.string.nc_server_product_name) + ) + ) + } else if (!status.version!!.startsWith("13.")) { + setErrorText( + String.format( + resources!!.getString(R.string.nc_server_version), + resources!!.getString(R.string.nc_app_product_name), + resources!!.getString(R.string.nc_server_product_name) + ) + ) + } + } + + private fun findServerTalkApp(queryUrl: String) { + ncApi.getCapabilities(ApiUtils.getUrlForCapabilities(queryUrl)) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(capabilitiesOverall: CapabilitiesOverall) { + val capabilities = capabilitiesOverall.ocs?.data?.capabilities + + val hasTalk = + capabilities?.spreedCapability != null && + capabilities.spreedCapability?.features != null && + capabilities.spreedCapability?.features?.isNotEmpty() == true + + if (hasTalk) { + runOnUiThread { + if (CapabilitiesUtil.isServerEOL(capabilitiesOverall.ocs?.data?.serverVersion?.major)) { + if (resources != null) { + runOnUiThread { + setErrorText(resources!!.getString(R.string.nc_settings_server_eol)) + } + } + } else { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_BASE_URL, queryUrl.replace("/status.php", "")) + + val intent = Intent(context, WebViewLoginActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } + } + } else { + if (resources != null) { + runOnUiThread { + setErrorText(resources!!.getString(R.string.nc_server_unsupported)) + } + } + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error while checking capabilities", e) + if (resources != null) { + runOnUiThread { + setErrorText(resources!!.getString(R.string.nc_common_error_sorry)) + } + } + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun isServerStatusQueryable(status: Status): Boolean = + status.installed && !status.maintenance && !status.needsUpgrade + + private fun setErrorText(text: String?) { + binding.errorWrapper.visibility = View.VISIBLE + binding.errorText.text = text + hideserverEntryProgressBar() + } + + private fun showserverEntryProgressBar() { + binding.errorWrapper.visibility = View.INVISIBLE + binding.serverEntryProgressBar.visibility = View.VISIBLE + } + + private fun hideserverEntryProgressBar() { + binding.serverEntryProgressBar.visibility = View.INVISIBLE + } + + @SuppressLint("LongLogTag") + private fun setCertTextView() { + runOnUiThread { + if (!TextUtils.isEmpty(appPreferences.temporaryClientCertAlias)) { + binding.certTextView.setText(R.string.nc_change_cert_auth) + } else { + binding.certTextView.setText(R.string.nc_configure_cert_auth) + } + hideserverEntryProgressBar() + } + } + + private val requestCameraPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> + if (isGranted) { + // Permission was granted + startQRScanner() + } + } + + fun onScan() { + if (PermissionUtil.isPermissionGranted(this, Manifest.permission.CAMERA)) { + startQRScanner() + } else { + requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) + } + } + + private fun startQRScanner() { + val intent = Intent(this, QrCodeActivity::class.java) + qrScanResultLauncher.launch(intent) + } + + private val qrScanResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val data = result.data + + if (data == null) { + return@registerForActivityResult + } + + val resultData = data.getStringExtra(QR_URI) + + if (resultData == null || !resultData.startsWith("nc")) { + Snackbar.make(binding.root, getString(R.string.qr_code_error), Snackbar.LENGTH_SHORT).show() + return@registerForActivityResult + } + + val intent = Intent(this, WebViewLoginActivity::class.java) + val bundle = bundleOf().apply { + putString(BundleKeys.KEY_FROM_QR, resultData) + } + intent.putExtras(bundle) + startActivity(intent) + } + } + + public override fun onDestroy() { + super.onDestroy() + dispose() + } + + private fun dispose() { + if (statusQueryDisposable != null && !statusQueryDisposable!!.isDisposed) { + statusQueryDisposable!!.dispose() + } + statusQueryDisposable = null + } + + override val appBarLayoutType: AppBarLayoutType + get() = AppBarLayoutType.EMPTY + + companion object { + private val TAG = ServerSelectionActivity::class.java.simpleName + const val MIN_SERVER_MAJOR_VERSION = 13 + private const val QR_URI = "com.blikoon.qrcodescanner.got_qr_scan_relult" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt new file mode 100644 index 0000000..81dcd99 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt @@ -0,0 +1,181 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.account + +import android.accounts.Account +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Bundle +import androidx.core.graphics.drawable.toDrawable +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.adapters.items.AdvancedUserItem +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivitySwitchAccountBinding +import com.nextcloud.talk.models.ImportAccount +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.AccountUtils.findAvailableAccountsOnDevice +import com.nextcloud.talk.utils.AccountUtils.getInformationFromAccount +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import org.osmdroid.config.Configuration +import java.net.CookieManager +import javax.inject.Inject + +/** + * Parts related to account import were either copied from or inspired by the great work done by David Luhmer at: + * https://github.com/nextcloud/ownCloud-Account-Importer + */ +@AutoInjector(NextcloudTalkApplication::class) +class SwitchAccountActivity : BaseActivity() { + private lateinit var binding: ActivitySwitchAccountBinding + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var cookieManager: CookieManager + + private var adapter: FlexibleAdapter>? = null + private val userItems: MutableList> = ArrayList() + private var isAccountImport = false + + private val onImportItemClickListener = FlexibleAdapter.OnItemClickListener { _, position -> + if (userItems.size > position) { + val account = (userItems[position] as AdvancedUserItem).account + reauthorizeFromImport(account) + } + true + } + + private val onSwitchItemClickListener = FlexibleAdapter.OnItemClickListener { _, position -> + if (userItems.size > position) { + val user = (userItems[position] as AdvancedUserItem).user + + if (userManager.setUserAsActive(user!!).blockingGet()) { + cookieManager.cookieStore.removeAll() + finish() + } + } + true + } + + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + binding = ActivitySwitchAccountBinding.inflate(layoutInflater) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + setContentView(binding.root) + setupActionBar() + initSystemBars() + + Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context)) + + handleIntent() + } + + private fun handleIntent() { + intent.extras?.let { + if (it.containsKey(KEY_IS_ACCOUNT_IMPORT)) { + isAccountImport = true + } + } + } + + private fun setupActionBar() { + setSupportActionBar(binding.toolbar) + binding.toolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(R.color.transparent, null).toDrawable()) + supportActionBar?.title = resources!!.getString(R.string.nc_select_an_account) + } + + @Suppress("Detekt.NestedBlockDepth") + override fun onResume() { + super.onResume() + + if (adapter == null) { + adapter = FlexibleAdapter(userItems, this, false) + var participant: Participant + + if (!isAccountImport) { + for (user in userManager.users.blockingGet()) { + if (!user.current) { + val userId: String? = if (user.userId != null) { + user.userId + } else { + user.username + } + participant = Participant() + participant.actorType = Participant.ActorType.USERS + participant.actorId = userId + participant.displayName = user.displayName + userItems.add(AdvancedUserItem(participant, user, null, viewThemeUtils, 0)) + } + } + adapter!!.addListener(onSwitchItemClickListener) + adapter!!.updateDataSet(userItems, false) + } else { + var account: Account + var importAccount: ImportAccount + var user: User + for (accountObject in findAvailableAccountsOnDevice(userManager.users.blockingGet())) { + account = accountObject + importAccount = getInformationFromAccount(account) + participant = Participant() + participant.actorType = Participant.ActorType.USERS + participant.actorId = importAccount.getUsername() + participant.displayName = importAccount.getUsername() + user = User() + user.baseUrl = importAccount.getBaseUrl() + userItems.add(AdvancedUserItem(participant, user, account, viewThemeUtils, 0)) + } + adapter!!.addListener(onImportItemClickListener) + adapter!!.updateDataSet(userItems, false) + } + } + prepareViews() + } + + private fun prepareViews() { + val layoutManager: LinearLayoutManager = SmoothScrollLinearLayoutManager(this) + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.adapter = adapter + } + + private fun reauthorizeFromImport(account: Account?) { + val importAccount = getInformationFromAccount(account!!) + val bundle = Bundle() + bundle.putString(KEY_BASE_URL, importAccount.getBaseUrl()) + bundle.putString(KEY_USERNAME, importAccount.getUsername()) + bundle.putString(KEY_TOKEN, importAccount.getToken()) + bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true) + + val intent = Intent(context, AccountVerificationActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt b/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt new file mode 100644 index 0000000..0d52529 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt @@ -0,0 +1,471 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Your Name + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.account + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ActivityInfo +import android.graphics.Bitmap +import android.net.http.SslError +import android.os.Build +import android.os.Bundle +import android.security.KeyChain +import android.security.KeyChainException +import android.text.TextUtils +import android.util.Log +import android.view.View +import android.webkit.ClientCertRequest +import android.webkit.CookieSyncManager +import android.webkit.SslErrorHandler +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.OnBackPressedCallback +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.activities.MainActivity +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.databinding.ActivityWebViewLoginBinding +import com.nextcloud.talk.events.CertificateEvent +import com.nextcloud.talk.jobs.AccountRemovalWorker +import com.nextcloud.talk.models.LoginData +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME +import com.nextcloud.talk.utils.ssl.TrustManager +import de.cotech.hw.fido.WebViewFidoBridge +import de.cotech.hw.fido2.WebViewWebauthnBridge +import de.cotech.hw.fido2.ui.WebauthnDialogOptions +import io.reactivex.disposables.Disposable +import java.lang.reflect.Field +import java.net.CookieManager +import java.net.URLDecoder +import java.security.PrivateKey +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.Locale +import javax.inject.Inject + +@Suppress("ReturnCount", "LongMethod") +@AutoInjector(NextcloudTalkApplication::class) +class WebViewLoginActivity : BaseActivity() { + + private lateinit var binding: ActivityWebViewLoginBinding + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var trustManager: TrustManager + + @Inject + lateinit var cookieManager: CookieManager + + private var assembledPrefix: String? = null + private var userQueryDisposable: Disposable? = null + private var baseUrl: String? = null + private var reauthorizeAccount = false + private var username: String? = null + private var password: String? = null + private var loginStep = 0 + private var automatedLoginAttempted = false + private var webViewFidoBridge: WebViewFidoBridge? = null + private var webViewWebauthnBridge: WebViewWebauthnBridge? = null + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + } + private val webLoginUserAgent: String + get() = ( + Build.MANUFACTURER.substring(0, 1).uppercase(Locale.getDefault()) + + Build.MANUFACTURER.substring(1).uppercase(Locale.getDefault()) + + " " + + Build.MODEL + + " (" + + resources!!.getString(R.string.nc_app_product_name) + + ")" + ) + + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + binding = ActivityWebViewLoginBinding.inflate(layoutInflater) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + setContentView(binding.root) + actionBar?.hide() + initSystemBars() + assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/" + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + handleIntent() + } + + private fun handleIntent() { + val extras = intent.extras!! + baseUrl = extras.getString(KEY_BASE_URL) + username = extras.getString(KEY_USERNAME) + + if (extras.containsKey(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)) { + reauthorizeAccount = extras.getBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT) + } + + if (extras.containsKey(BundleKeys.KEY_PASSWORD)) { + password = extras.getString(BundleKeys.KEY_PASSWORD) + } + + if (extras.containsKey(BundleKeys.KEY_FROM_QR)) { + extras.getString(BundleKeys.KEY_FROM_QR)?.let { + parseAndLoginFromWebView(it) + } + } else { + setupWebView() + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun setupWebView() { + binding.webview.settings.allowFileAccess = false + binding.webview.settings.allowFileAccessFromFileURLs = false + binding.webview.settings.javaScriptEnabled = true + binding.webview.settings.javaScriptCanOpenWindowsAutomatically = false + binding.webview.settings.domStorageEnabled = true + binding.webview.settings.userAgentString = webLoginUserAgent + binding.webview.settings.saveFormData = false + binding.webview.settings.savePassword = false + binding.webview.settings.setRenderPriority(WebSettings.RenderPriority.HIGH) + binding.webview.clearCache(true) + binding.webview.clearFormData() + binding.webview.clearHistory() + WebView.clearClientCertPreferences(null) + webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(this, binding.webview) + + val webauthnOptionsBuilder = WebauthnDialogOptions.builder().setShowSdkLogo(true).setAllowSkipPin(true) + webViewWebauthnBridge = WebViewWebauthnBridge.createInstanceForWebView( + this, + binding.webview, + webauthnOptionsBuilder + ) + + CookieSyncManager.createInstance(this) + android.webkit.CookieManager.getInstance().removeAllCookies(null) + val headers: MutableMap = HashMap() + headers["OCS-APIRequest"] = "true" + binding.webview.webViewClient = object : WebViewClient() { + private var basePageLoaded = false + override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { + webViewFidoBridge?.delegateShouldInterceptRequest(view, request) + webViewWebauthnBridge?.delegateShouldInterceptRequest(view, request) + return super.shouldInterceptRequest(view, request) + } + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + webViewFidoBridge?.delegateOnPageStarted(view, url, favicon) + webViewWebauthnBridge?.delegateOnPageStarted(view, url, favicon) + } + + @Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)") + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + if (url.startsWith(assembledPrefix!!)) { + parseAndLoginFromWebView(url) + return true + } + return false + } + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onPageFinished(view: WebView, url: String) { + loginStep++ + if (!basePageLoaded) { + binding.progressBar.visibility = View.GONE + binding.webview.visibility = View.VISIBLE + + basePageLoaded = true + } + if (!TextUtils.isEmpty(username)) { + if (loginStep == 1) { + binding.webview.loadUrl( + "javascript: {document.getElementsByClassName('login')[0].click(); };" + ) + } else if (!automatedLoginAttempted) { + automatedLoginAttempted = true + if (TextUtils.isEmpty(password)) { + binding.webview.loadUrl( + "javascript:var justStore = document.getElementById('user').value = '$username';" + ) + } else { + binding.webview.loadUrl( + "javascript: {" + + "document.getElementById('user').value = '" + username + "';" + + "document.getElementById('password').value = '" + password + "';" + + "document.getElementById('submit').click(); };" + ) + } + } + } + + super.onPageFinished(view, url) + } + + override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) { + var alias: String? = null + if (!reauthorizeAccount) { + alias = appPreferences.temporaryClientCertAlias + } + val user = currentUserProvider.currentUser.blockingGet() + if (TextUtils.isEmpty(alias) && user != null) { + alias = user.clientCertificate + } + if (!TextUtils.isEmpty(alias)) { + val finalAlias = alias + Thread { + try { + val privateKey = KeyChain.getPrivateKey(applicationContext, finalAlias!!) + val certificates = KeyChain.getCertificateChain( + applicationContext, + finalAlias + ) + if (privateKey != null && certificates != null) { + request.proceed(privateKey, certificates) + } else { + request.cancel() + } + } catch (e: KeyChainException) { + request.cancel() + } catch (e: InterruptedException) { + request.cancel() + } + }.start() + } else { + KeyChain.choosePrivateKeyAlias( + this@WebViewLoginActivity, + { chosenAlias: String? -> + if (chosenAlias != null) { + appPreferences!!.temporaryClientCertAlias = chosenAlias + Thread { + var privateKey: PrivateKey? = null + try { + privateKey = KeyChain.getPrivateKey(applicationContext, chosenAlias) + val certificates = KeyChain.getCertificateChain( + applicationContext, + chosenAlias + ) + if (privateKey != null && certificates != null) { + request.proceed(privateKey, certificates) + } else { + request.cancel() + } + } catch (e: KeyChainException) { + request.cancel() + } catch (e: InterruptedException) { + request.cancel() + } + }.start() + } else { + request.cancel() + } + }, + arrayOf("RSA", "EC"), + null, + request.host, + request.port, + null + ) + } + } + + @SuppressLint("DiscouragedPrivateApi") + @Suppress("Detekt.TooGenericExceptionCaught", "WebViewClientOnReceivedSslError") + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { + try { + val sslCertificate = error.certificate + val f: Field = sslCertificate.javaClass.getDeclaredField("mX509Certificate") + f.isAccessible = true + val cert = f[sslCertificate] as X509Certificate + try { + trustManager.checkServerTrusted(arrayOf(cert), "generic") + handler.proceed() + } catch (exception: CertificateException) { + eventBus.post(CertificateEvent(cert, trustManager, handler)) + } + } catch (exception: Exception) { + handler.cancel() + } + } + + @Deprecated("Deprecated in super implementation") + override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { + super.onReceivedError(view, errorCode, description, failingUrl) + } + } + binding.webview.loadUrl("$baseUrl/index.php/login/flow", headers) + } + + private fun dispose() { + if (userQueryDisposable != null && !userQueryDisposable!!.isDisposed) { + userQueryDisposable!!.dispose() + } + userQueryDisposable = null + } + + private fun parseAndLoginFromWebView(dataString: String) { + val loginData = parseLoginData(assembledPrefix, dataString) + if (loginData != null) { + dispose() + cookieManager.cookieStore.removeAll() + + if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, loginData.serverUrl!!) + .blockingGet() + ) { + Log.e(TAG, "Tried to add already existing user who is scheduled for deletion.") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + // however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it. + startAccountRemovalWorkerAndRestartApp() + } else if (userManager.checkIfUserExists(loginData.username!!, loginData.serverUrl!!) + .blockingGet() + ) { + if (reauthorizeAccount) { + updateUserAndRestartApp(loginData) + } else { + Log.w(TAG, "It was tried to add an account that account already exists. Skipped user creation.") + restartApp() + } + } else { + startAccountVerification(loginData) + } + } else { + Log.e(TAG, "Login Data was null") + restartApp() + } + } + + private fun startAccountVerification(loginData: LoginData) { + val bundle = Bundle() + bundle.putString(KEY_USERNAME, loginData.username) + bundle.putString(KEY_TOKEN, loginData.token) + bundle.putString(KEY_BASE_URL, loginData.serverUrl) + var protocol = "" + if (loginData.serverUrl!!.startsWith("http://")) { + protocol = "http://" + } else if (loginData.serverUrl!!.startsWith("https://")) { + protocol = "https://" + } + if (!TextUtils.isEmpty(protocol)) { + bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol) + } + val intent = Intent(context, AccountVerificationActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } + + private fun restartApp() { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + + private fun updateUserAndRestartApp(loginData: LoginData) { + val currentUser = currentUserProvider.currentUser.blockingGet() + if (currentUser != null) { + currentUser.clientCertificate = appPreferences.temporaryClientCertAlias + currentUser.token = loginData.token + val rowsUpdated = userManager.updateOrCreateUser(currentUser).blockingGet() + Log.d(TAG, "User rows updated: $rowsUpdated") + restartApp() + } + } + + private fun startAccountRemovalWorkerAndRestartApp() { + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo? -> + + when (workInfo?.state) { + WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + restartApp() + } + + else -> {} + } + } + } + + private fun parseLoginData(prefix: String?, dataString: String): LoginData? { + if (dataString.length < prefix!!.length) { + return null + } + val loginData = LoginData() + + // format is xxx://login/server:xxx&user:xxx&password:xxx + val data: String = dataString.substring(prefix.length) + val values: Array = data.split("&").toTypedArray() + if (values.size != PARAMETER_COUNT) { + return null + } + for (value in values) { + if (value.startsWith("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) { + loginData.username = URLDecoder.decode( + value.substring(("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length) + ) + } else if (value.startsWith("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) { + loginData.token = URLDecoder.decode( + value.substring(("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length) + ) + } else if (value.startsWith("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) { + loginData.serverUrl = URLDecoder.decode( + value.substring(("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR").length) + ) + } else { + return null + } + } + return if (!TextUtils.isEmpty(loginData.serverUrl) && + !TextUtils.isEmpty(loginData.username) && + !TextUtils.isEmpty(loginData.token) + ) { + loginData + } else { + null + } + } + + public override fun onDestroy() { + super.onDestroy() + dispose() + } + + init { + sharedApplication!!.componentApplication.inject(this) + } + + override val appBarLayoutType: AppBarLayoutType + get() = AppBarLayoutType.EMPTY + + companion object { + private val TAG = WebViewLoginActivity::class.java.simpleName + private const val PROTOCOL_SUFFIX = "://" + private const val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":" + private const val PARAMETER_COUNT = 3 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt b/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt new file mode 100644 index 0000000..d2ddf88 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/account/data/LoginRepository.kt @@ -0,0 +1,161 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.account.data + +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import com.nextcloud.talk.account.data.io.LocalLoginDataSource +import com.nextcloud.talk.account.data.model.LoginCompletion +import com.nextcloud.talk.account.data.model.LoginResponse +import com.nextcloud.talk.account.data.network.NetworkLoginDataSource +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import java.net.URLDecoder + +@Suppress("TooManyFunctions", "ReturnCount") +class LoginRepository(val network: NetworkLoginDataSource, val local: LocalLoginDataSource) { + + companion object { + val TAG: String = LoginRepository::class.java.simpleName + private const val INTERVAL = 250L + private const val HTTP_OK = 200 + private const val USER_KEY = "user:" + private const val SERVER_KEY = "server:" + private const val PASS_KEY = "password:" + private const val PREFIX = "nc://login/" + private const val MAX_ARGS = 3 + } + + private var shouldReauthorizeUser = false + private var shouldLoop = true + + suspend fun pollLogin(response: LoginResponse): LoginCompletion? = + withContext(Dispatchers.IO) { + while (shouldLoop) { + val loginData = network.performLoginFlowV2(response) + if (loginData == null) { + break + } + + if (loginData.status == HTTP_OK) { + return@withContext loginData + } + + delay(INTERVAL) // No response yet, retry + } + return@withContext null + } + + /** + * Entry point for QR scanner + * + */ + fun startLoginFlowFromQR(dataString: String, reAuth: Boolean = false): LoginCompletion? { + shouldReauthorizeUser = reAuth + + if (!dataString.startsWith(PREFIX)) { + Log.e(TAG, "Invalid login URL detected") + return null + } + + val data = dataString.removePrefix(PREFIX) + val values = data.split('&') + + if (values.size !in 1..MAX_ARGS) { + Log.e(TAG, "Illegal number of login URL elements detected: ${values.size}") + return null + } + + var server = "" + var loginName = "" + var appPassword = "" + values.forEach { value -> + when { + value.startsWith(USER_KEY) -> { + loginName = URLDecoder.decode(value.removePrefix(USER_KEY), "UTF-8") + } + + value.startsWith(PASS_KEY) -> { + appPassword = URLDecoder.decode(value.removePrefix(PASS_KEY), "UTF-8") + } + + value.startsWith(SERVER_KEY) -> { + server = URLDecoder.decode(value.removePrefix(SERVER_KEY), "UTF-8") + } + } + } + + return if (server.isNotEmpty() && loginName.isNotEmpty() && appPassword.isNotEmpty()) { + LoginCompletion(HTTP_OK, server, loginName, appPassword) + } else { + null + } + } + + /** + * Entry point to the login process + */ + suspend fun startLoginFlow(baseUrl: String, reAuth: Boolean = false): LoginResponse? = + withContext(Dispatchers.IO) { + shouldReauthorizeUser = reAuth + val response = network.anonymouslyPostLoginRequest(baseUrl) + return@withContext response + } + + /** + * Ends normal login process by canceling the polling + */ + fun cancelLoginFlow() { + shouldLoop = false + } + + /** + * Returns bundle if user is not scheduled for deletion or doesn't already exist, null otherwise + */ + fun parseAndLogin(loginData: LoginCompletion): Bundle? { + if (local.checkIfUserIsScheduledForDeletion(loginData)) { + // however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it. + local.startAccountRemovalWorker() + return null + } else if (local.checkIfUserExists(loginData)) { + if (shouldReauthorizeUser) { + local.updateUser(loginData) + } else { + Log.w(TAG, "Tried to add an account that account already exists. Skipped user creation.") + } + + return null + } else { + return startAccountVerification(loginData) + } + } + + private fun startAccountVerification(loginData: LoginCompletion): Bundle { + val bundle = Bundle() + bundle.putString(KEY_USERNAME, loginData.loginName) + bundle.putString(KEY_TOKEN, loginData.appPassword) + bundle.putString(KEY_BASE_URL, loginData.server) + var protocol = "" + if (loginData.server.startsWith("http://")) { + protocol = "http://" + } else if (loginData.server.startsWith("https://")) { + protocol = "https://" + } + if (!TextUtils.isEmpty(protocol)) { + bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol) + } + + return bundle + } +} diff --git a/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt b/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt new file mode 100644 index 0000000..c7672e7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.account.data.io + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import com.nextcloud.talk.account.data.model.LoginCompletion +import com.nextcloud.talk.jobs.AccountRemovalWorker +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.preferences.AppPreferences + +// local datasource for communicating with room through account manager +// crucial for making sure the login process interacts with the db as expected. +class LocalLoginDataSource(val userManager: UserManager, val appPreferences: AppPreferences, val context: Context) { + + fun updateUser(loginData: LoginCompletion) { + val currentUser = userManager.currentUser.blockingGet() + if (currentUser != null) { + currentUser.clientCertificate = appPreferences.temporaryClientCertAlias + currentUser.token = loginData.appPassword + userManager.updateOrCreateUser(currentUser) + } + } + + fun startAccountRemovalWorker(): LiveData { + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance(context).enqueue(accountRemovalWork) + + return WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + } + + fun checkIfUserIsScheduledForDeletion(data: LoginCompletion): Boolean = + userManager.checkIfUserIsScheduledForDeletion(data.loginName, data.server).blockingGet() + + fun checkIfUserExists(data: LoginCompletion): Boolean = + userManager.checkIfUserExists(data.loginName, data.server).blockingGet() +} diff --git a/app/src/main/java/com/nextcloud/talk/account/data/model/LoginResponseModels.kt b/app/src/main/java/com/nextcloud/talk/account/data/model/LoginResponseModels.kt new file mode 100644 index 0000000..346a5ff --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/account/data/model/LoginResponseModels.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.account.data.model + +data class LoginResponse(val token: String, val pollUrl: String, val loginUrl: String) + +data class LoginCompletion(val status: Int, val server: String, val loginName: String, val appPassword: String) diff --git a/app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt b/app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt new file mode 100644 index 0000000..aadd601 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/account/data/network/NetworkLoginDataSource.kt @@ -0,0 +1,122 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.account.data.network + +import android.util.Log +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.nextcloud.talk.account.data.model.LoginCompletion +import com.nextcloud.talk.account.data.model.LoginResponse +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import java.io.IOException +import javax.net.ssl.SSLHandshakeException + +// This class handles the network and polling logic in isolation, which makes it easier to test +// Login and Authentication is critical, thus it needs to be working properly. +class NetworkLoginDataSource(val okHttpClient: OkHttpClient) { + + companion object { + val TAG: String = NetworkLoginDataSource::class.java.simpleName + } + + fun anonymouslyPostLoginRequest(baseUrl: String): LoginResponse? { + val url = "$baseUrl/index.php/login/v2" + var result: LoginResponse? = null + runCatching { + val response = getResponseOfAnonymouslyPostLoginRequest(url) + val jsonObject: JsonObject = JsonParser.parseString(response).asJsonObject + val loginUrl: String = getLoginUrl(jsonObject) + val token = jsonObject.getAsJsonObject("poll").get("token").asString + val pollUrl = jsonObject.getAsJsonObject("poll").get("endpoint").asString + result = LoginResponse(token, pollUrl, loginUrl) + }.getOrElse { e -> + when (e) { + is SSLHandshakeException, + is NullPointerException, + is IOException -> { + Log.e(TAG, "Error caught at anonymouslyPostLoginRequest: $e") + } + + else -> throw e + } + } + + return result + } + + private fun getResponseOfAnonymouslyPostLoginRequest(url: String): String? { + val request = Request.Builder() + .url(url) + .post(FormBody.Builder().build()) + .addHeader("Clear-Site-Data", "cookies") + .build() + + okHttpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("Unexpected code $response") + } + return response.body?.string() + } + } + + private fun getLoginUrl(response: JsonObject): String { + var result: String? = response.get("login").asString + if (result == null) { + result = "" + } + + return result + } + + fun performLoginFlowV2(response: LoginResponse): LoginCompletion? { + val requestBody: RequestBody = FormBody.Builder() + .add("token", response.token) + .build() + + val request = Request.Builder() + .url(response.pollUrl) + .post(requestBody) + .build() + + var result: LoginCompletion? = null + runCatching { + okHttpClient.newCall(request).execute() + .use { response -> + val status: Int = response.code + val responseBody = response.body?.string() + + result = if (response.isSuccessful && responseBody?.isNotEmpty() == true) { + val jsonObject = JsonParser.parseString(responseBody).asJsonObject + val server: String = jsonObject.get("server").asString + val loginName: String = jsonObject.get("loginName").asString + val appPassword: String = jsonObject.get("appPassword").asString + + LoginCompletion(status, server, loginName, appPassword) + } else { + LoginCompletion(status, "", "", "") + } + } + }.getOrElse { e -> + when (e) { + is NullPointerException, + is SSLHandshakeException, + is IllegalStateException, + is IOException -> { + Log.e(TAG, "Error caught at performLoginFlowV2: $e") + } + + else -> throw e + } + } + + return result + } +} diff --git a/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt b/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt new file mode 100644 index 0000000..b76b27c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/account/viewmodels/BrowserLoginActivityViewModel.kt @@ -0,0 +1,92 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Your Name + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.account.viewmodels + +import android.os.Bundle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.account.data.LoginRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class BrowserLoginActivityViewModel @Inject constructor(val repository: LoginRepository) : ViewModel() { + + companion object { + private val TAG = BrowserLoginActivityViewModel::class.java.simpleName + } + + sealed class InitialLoginViewState { + data object None : InitialLoginViewState() + data class InitialLoginRequestSuccess(val loginUrl: String) : InitialLoginViewState() + data object InitialLoginRequestError : InitialLoginViewState() + } + + private val _initialLoginRequestState = MutableStateFlow(InitialLoginViewState.None) + val initialLoginRequestState: StateFlow = _initialLoginRequestState + + sealed class PostLoginViewState { + data object None : PostLoginViewState() + data object PostLoginRestartApp : PostLoginViewState() + data object PostLoginError : PostLoginViewState() + data class PostLoginContinue(val data: Bundle) : PostLoginViewState() + } + + private val _postLoginState = MutableStateFlow(PostLoginViewState.None) + val postLoginState: StateFlow = _postLoginState + + fun loginNormally(baseUrl: String, reAuth: Boolean = false) { + viewModelScope.launch { + val response = repository.startLoginFlow(baseUrl, reAuth) + + if (response == null) { + _initialLoginRequestState.value = InitialLoginViewState.InitialLoginRequestError + return@launch + } + + _initialLoginRequestState.value = + InitialLoginViewState.InitialLoginRequestSuccess(response.loginUrl) + + val loginCompletionResponse = repository.pollLogin(response) + + if (loginCompletionResponse == null) { + _postLoginState.value = PostLoginViewState.PostLoginError + return@launch + } + + val bundle = repository.parseAndLogin(loginCompletionResponse) + if (bundle == null) { + _postLoginState.value = PostLoginViewState.PostLoginRestartApp + return@launch + } + + _postLoginState.value = PostLoginViewState.PostLoginContinue(bundle) + } + } + + fun loginWithQR(dataString: String, reAuth: Boolean = false) { + viewModelScope.launch { + val loginCompletionResponse = repository.startLoginFlowFromQR(dataString, reAuth) + if (loginCompletionResponse == null) { + _postLoginState.value = PostLoginViewState.PostLoginError + return@launch + } + + val bundle = repository.parseAndLogin(loginCompletionResponse) + if (bundle == null) { + _postLoginState.value = PostLoginViewState.PostLoginRestartApp + return@launch + } + + _postLoginState.value = PostLoginViewState.PostLoginContinue(bundle) + } + } + + fun cancelLogin() = repository.cancelLoginFlow() +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/ActionBarProvider.java b/app/src/main/java/com/nextcloud/talk/activities/ActionBarProvider.java new file mode 100644 index 0000000..9713f26 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/ActionBarProvider.java @@ -0,0 +1,13 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2016 BlueLine Labs, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nextcloud.talk.activities; + +import androidx.appcompat.app.ActionBar; + +public interface ActionBarProvider { + ActionBar getSupportActionBar(); +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt new file mode 100644 index 0000000..0b203e3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt @@ -0,0 +1,295 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.activities + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.inputmethod.EditorInfo +import android.webkit.SslErrorHandler +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.account.AccountVerificationActivity +import com.nextcloud.talk.account.ServerSelectionActivity +import com.nextcloud.talk.account.SwitchAccountActivity +import com.nextcloud.talk.account.WebViewLoginActivity +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.events.CertificateEvent +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.FileViewerUtils +import com.nextcloud.talk.utils.UriUtils +import com.nextcloud.talk.utils.adjustUIForAPILevel35 +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.nextcloud.talk.utils.ssl.TrustManager +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.security.cert.CertificateParsingException +import java.security.cert.X509Certificate +import java.text.DateFormat +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +open class BaseActivity : AppCompatActivity() { + + enum class AppBarLayoutType { + TOOLBAR, + SEARCH_BAR, + EMPTY + } + + @Inject + lateinit var eventBus: EventBus + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var context: Context + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + open val appBarLayoutType: AppBarLayoutType + get() = AppBarLayoutType.TOOLBAR + + open val view: View? + get() = null + + override fun onCreate(savedInstanceState: Bundle?) { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + adjustUIForAPILevel35() + super.onCreate(savedInstanceState) + + cleanTempCertPreference() + } + + public override fun onStart() { + super.onStart() + eventBus.register(this) + } + + public override fun onResume() { + super.onResume() + + if (appPreferences.isKeyboardIncognito) { + val viewGroup = (findViewById(android.R.id.content) as ViewGroup).getChildAt(0) as ViewGroup + disableKeyboardPersonalisedLearning(viewGroup) + } + + if (appPreferences.isScreenSecured || appPreferences.isScreenLocked) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + + public override fun onStop() { + super.onStop() + eventBus.unregister(this) + } + + /* + * May be aligned with android-common lib in the future: .../ui/util/extensions/AppCompatActivityExtensions.kt + */ + fun initSystemBars() { + val decorView = window.decorView + decorView.setOnApplyWindowInsetsListener { view, insets -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + val color = ResourcesCompat.getColor(resources, R.color.bg_default, context.theme) + view.setBackgroundColor(color) + view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + } else { + colorizeStatusBar() + colorizeNavigationBar() + } + insets + } + ViewCompat.requestApplyInsets(decorView) + } + + open fun colorizeStatusBar() { + if (resources != null) { + if (appBarLayoutType == AppBarLayoutType.SEARCH_BAR) { + viewThemeUtils.platform.resetStatusBar(this) + } else { + viewThemeUtils.platform.themeStatusBar(this) + } + } + } + + fun colorizeNavigationBar() { + if (resources != null) { + DisplayUtils.applyColorToNavigationBar( + this.window, + ResourcesCompat.getColor(resources, R.color.bg_default, null) + ) + } + } + + private fun disableKeyboardPersonalisedLearning(viewGroup: ViewGroup) { + var view: View? + var editText: EditText + for (i in 0 until viewGroup.childCount) { + view = viewGroup.getChildAt(i) + if (view is EditText) { + editText = view + editText.imeOptions = editText.imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } else if (view is ViewGroup) { + disableKeyboardPersonalisedLearning(view) + } + } + } + + @Suppress("Detekt.NestedBlockDepth") + private fun showCertificateDialog( + cert: X509Certificate, + trustManager: TrustManager, + sslErrorHandler: SslErrorHandler? + ) { + val formatter = DateFormat.getDateInstance(DateFormat.LONG) + val validFrom = formatter.format(cert.notBefore) + val validUntil = formatter.format(cert.notAfter) + + val issuedBy = cert.issuerDN.toString() + val issuedFor: String + + try { + if (cert.subjectAlternativeNames != null) { + val stringBuilder = StringBuilder() + for (o in cert.subjectAlternativeNames) { + val list = o as List<*> + val type = list[0] as Int + if (type == 2) { + val name = list[1] as String + stringBuilder.append("[").append(type).append("]").append(name).append(" ") + } + } + issuedFor = stringBuilder.toString() + } else { + issuedFor = cert.subjectDN.name + } + + @SuppressLint("StringFormatMatches") + val dialogText = String.format( + resources.getString(R.string.nc_certificate_dialog_text), + issuedBy, + issuedFor, + validFrom, + validUntil + ) + + val dialogBuilder = MaterialAlertDialogBuilder(this).setIcon( + viewThemeUtils.dialog.colorMaterialAlertDialogIcon( + context, + R.drawable.ic_security_white_24dp + ) + ).setTitle(R.string.nc_certificate_dialog_title) + .setMessage(dialogText) + .setPositiveButton(R.string.nc_yes) { _, _ -> + trustManager.addCertInTrustStore(cert) + sslErrorHandler?.proceed() + }.setNegativeButton(R.string.nc_no) { _, _ -> + sslErrorHandler?.cancel() + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(context, dialogBuilder) + + val dialog = dialogBuilder.show() + + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } catch (e: CertificateParsingException) { + Log.d(TAG, "Failed to parse the certificate") + } + } + + private fun cleanTempCertPreference() { + val temporaryClassNames: MutableList = ArrayList() + temporaryClassNames.add(ServerSelectionActivity::class.java.name) + temporaryClassNames.add(AccountVerificationActivity::class.java.name) + temporaryClassNames.add(WebViewLoginActivity::class.java.name) + temporaryClassNames.add(SwitchAccountActivity::class.java.name) + if (!temporaryClassNames.contains(javaClass.name)) { + appPreferences.removeTemporaryClientCertAlias() + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(event: CertificateEvent) { + showCertificateDialog(event.x509Certificate, event.trustManager, event.sslErrorHandler) + } + + override fun startActivity(intent: Intent) { + val user = currentUserProvider.currentUser.blockingGet() + if (intent.data != null && TextUtils.equals(intent.action, Intent.ACTION_VIEW)) { + val uri = intent.data.toString() + if (user?.baseUrl != null && uri.startsWith(user.baseUrl!!)) { + if (UriUtils.isInstanceInternalFileShareUrl(user.baseUrl!!, uri)) { + // https://cloud.nextcloud.com/f/41 + val fileViewerUtils = FileViewerUtils(applicationContext, user) + fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileShareFileId(uri)) + } else if (UriUtils.isInstanceInternalFileUrl(user.baseUrl!!, uri)) { + // https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41 + val fileViewerUtils = FileViewerUtils(applicationContext, user) + fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileFileId(uri)) + } else if (UriUtils.isInstanceInternalFileUrlNew(user.baseUrl!!, uri)) { + // https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41 + val fileViewerUtils = FileViewerUtils(applicationContext, user) + fileViewerUtils.openFileInFilesApp(uri, UriUtils.extractInstanceInternalFileFileIdNew(uri)) + } else if (UriUtils.isInstanceInternalTalkUrl(user.baseUrl!!, uri)) { + // https://cloud.nextcloud.com/call/123456789 + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, UriUtils.extractRoomTokenFromTalkUrl(uri)) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(chatIntent) + } else { + super.startActivity(intent) + } + } else { + super.startActivity(intent) + } + } else { + super.startActivity(intent) + } + } + + companion object { + private val TAG = BaseActivity::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt new file mode 100644 index 0000000..6eb2c4f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -0,0 +1,3355 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.activities + +import android.Manifest +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.app.RemoteAction +import android.content.BroadcastReceiver +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.Icon +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaPlayer +import android.media.MediaRecorder +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.text.TextUtils +import android.text.format.DateUtils +import android.util.Log +import android.view.MotionEvent +import android.view.OrientationEventListener +import android.view.View +import android.view.View.OnTouchListener +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.RelativeLayout +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.DrawableRes +import androidx.appcompat.app.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateListOf +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.graphics.toColorInt +import androidx.core.net.toUri +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.bluelinelabs.logansquare.LoganSquare +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.ParticipantDisplayItem +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.call.CallParticipant +import com.nextcloud.talk.call.CallParticipantList +import com.nextcloud.talk.call.CallParticipantModel +import com.nextcloud.talk.call.LocalStateBroadcaster +import com.nextcloud.talk.call.LocalStateBroadcasterMcu +import com.nextcloud.talk.call.LocalStateBroadcasterNoMcu +import com.nextcloud.talk.call.MessageSender +import com.nextcloud.talk.call.MessageSenderMcu +import com.nextcloud.talk.call.MessageSenderNoMcu +import com.nextcloud.talk.call.MutableLocalCallParticipantModel +import com.nextcloud.talk.call.ReactionAnimator +import com.nextcloud.talk.call.components.ParticipantGrid +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.CallActivityBinding +import com.nextcloud.talk.events.ConfigurationChangeEvent +import com.nextcloud.talk.events.NetworkEvent +import com.nextcloud.talk.events.ProximitySensorEvent +import com.nextcloud.talk.events.WebSocketCommunicationEvent +import com.nextcloud.talk.models.ExternalSignalingServer +import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage +import com.nextcloud.talk.models.json.signaling.Signaling +import com.nextcloud.talk.models.json.signaling.SignalingOverall +import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall +import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel +import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel.LoweredHandState +import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel.RaisedHandState +import com.nextcloud.talk.signaling.SignalingMessageReceiver +import com.nextcloud.talk.signaling.SignalingMessageReceiver.CallParticipantMessageListener +import com.nextcloud.talk.signaling.SignalingMessageReceiver.LocalParticipantMessageListener +import com.nextcloud.talk.signaling.SignalingMessageReceiver.OfferMessageListener +import com.nextcloud.talk.signaling.SignalingMessageSender +import com.nextcloud.talk.ui.dialog.AudioOutputDialog +import com.nextcloud.talk.ui.dialog.MoreCallActionsDialog +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.CapabilitiesUtil.isCallRecordingAvailable +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationsForRoom +import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri +import com.nextcloud.talk.utils.ReceiverFlag +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.VibrationUtils.vibrateShort +import com.nextcloud.talk.utils.animations.PulseAnimation +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_PASSWORD +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MODIFIED_BASE_URL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_RECORDING_STATE +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ONE_TO_ONE +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil +import com.nextcloud.talk.utils.power.PowerManagerUtils +import com.nextcloud.talk.utils.registerPermissionHandlerBroadcastReceiver +import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder +import com.nextcloud.talk.viewmodels.CallRecordingViewModel +import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingConfirmStopState +import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingErrorState +import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingStartedState +import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingStartingState +import com.nextcloud.talk.webrtc.PeerConnectionWrapper +import com.nextcloud.talk.webrtc.PeerConnectionWrapper.PeerConnectionObserver +import com.nextcloud.talk.webrtc.WebRTCUtils +import com.nextcloud.talk.webrtc.WebRtcAudioManager +import com.nextcloud.talk.webrtc.WebRtcAudioManager.AudioDevice +import com.nextcloud.talk.webrtc.WebSocketConnectionHelper +import com.nextcloud.talk.webrtc.WebSocketInstance +import com.wooplr.spotlight.SpotlightView +import io.reactivex.Observable +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import okhttp3.Cache +import org.apache.commons.lang3.StringEscapeUtils +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.webrtc.AudioSource +import org.webrtc.AudioTrack +import org.webrtc.Camera1Enumerator +import org.webrtc.Camera2Enumerator +import org.webrtc.CameraEnumerator +import org.webrtc.CameraVideoCapturer +import org.webrtc.CameraVideoCapturer.CameraSwitchHandler +import org.webrtc.DefaultVideoDecoderFactory +import org.webrtc.DefaultVideoEncoderFactory +import org.webrtc.EglBase +import org.webrtc.Logging +import org.webrtc.MediaConstraints +import org.webrtc.MediaStream +import org.webrtc.PeerConnection +import org.webrtc.PeerConnection.IceConnectionState +import org.webrtc.PeerConnectionFactory +import org.webrtc.RendererCommon +import org.webrtc.SurfaceTextureHelper +import org.webrtc.VideoCapturer +import org.webrtc.VideoSource +import org.webrtc.VideoTrack +import java.io.IOException +import java.util.Objects +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.roundToInt + +@AutoInjector(NextcloudTalkApplication::class) +@Suppress("TooManyFunctions") +class CallActivity : CallBaseActivity() { + @JvmField + @Inject + var ncApi: NcApi? = null + + @JvmField + @Inject + var userManager: UserManager? = null + + @JvmField + @Inject + var cache: Cache? = null + + @JvmField + @Inject + var permissionUtil: PlatformPermissionUtil? = null + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + var audioManager: WebRtcAudioManager? = null + var callRecordingViewModel: CallRecordingViewModel? = null + var raiseHandViewModel: RaiseHandViewModel? = null + private var mReceiver: BroadcastReceiver? = null + private var peerConnectionFactory: PeerConnectionFactory? = null + private var audioConstraints: MediaConstraints? = null + private var videoConstraints: MediaConstraints? = null + private var sdpConstraints: MediaConstraints? = null + private var sdpConstraintsForMCUPublisher: MediaConstraints? = null + private var videoSource: VideoSource? = null + private var localVideoTrack: VideoTrack? = null + private var audioSource: AudioSource? = null + private var localAudioTrack: AudioTrack? = null + private var videoCapturer: VideoCapturer? = null + private var rootEglBase: EglBase? = null + private var signalingDisposable: Disposable? = null + private var iceServers: MutableList? = null + private var cameraEnumerator: CameraEnumerator? = null + private var roomToken: String? = null + lateinit var conversationUser: User + private var conversationName: String? = null + private var callSession: String? = null + private var localStream: MediaStream? = null + private var credentials: String? = null + private val peerConnectionWrapperList: MutableList = ArrayList() + private var videoOn = false + private var microphoneOn = false + private var isVoiceOnlyCall = false + private var isCallWithoutNotification = false + private var isIncomingCallFromNotification = false + private val callControlHandler = Handler() + private val callInfosHandler = Handler() + private val cameraSwitchHandler = Handler() + + private val callTimeHandler = Handler(Looper.getMainLooper()) + + // push to talk + private var isPushToTalkActive = false + private var pulseAnimation: PulseAnimation? = null + private var baseUrl: String? = null + private var roomId: String? = null + private var spotlightView: SpotlightView? = null + private val internalSignalingMessageReceiver = InternalSignalingMessageReceiver() + private var signalingMessageReceiver: SignalingMessageReceiver? = null + private val internalSignalingMessageSender = InternalSignalingMessageSender() + private var signalingMessageSender: SignalingMessageSender? = null + private var messageSender: MessageSender? = null + private val localCallParticipantModel: MutableLocalCallParticipantModel = MutableLocalCallParticipantModel() + private var localStateBroadcaster: LocalStateBroadcaster? = null + private val offerAnswerNickProviders: MutableMap = HashMap() + private val callParticipantMessageListeners: MutableMap = HashMap() + private val selfPeerConnectionObserver: PeerConnectionObserver = CallActivitySelfPeerConnectionObserver() + private var callParticipants: MutableMap = HashMap() + private val screenParticipantDisplayItemManagers: MutableMap = + HashMap() + private val screenParticipantDisplayItemManagersHandler = Handler(Looper.getMainLooper()) + private val callParticipantEventDisplayers: MutableMap = HashMap() + private val callParticipantEventDisplayersHandler = Handler(Looper.getMainLooper()) + private val callParticipantListObserver: CallParticipantList.Observer = object : CallParticipantList.Observer { + override fun onCallParticipantsChanged( + joined: Collection, + updated: Collection, + left: Collection, + unchanged: Collection + ) { + handleCallParticipantsChanged(joined, updated, left, unchanged) + } + + override fun onCallEndedForAll() { + Log.d(TAG, "A moderator ended the call for all.") + hangup(true, false) + } + } + private var callParticipantList: CallParticipantList? = null + private var switchToRoomToken = "" + private var isBreakoutRoom = false + private val localParticipantMessageListener = LocalParticipantMessageListener { token -> + switchToRoomToken = token + hangup(true, false) + } + private val offerMessageListener = OfferMessageListener { sessionId, roomType, sdp, nick -> + getOrCreatePeerConnectionWrapperForSessionIdAndType( + sessionId, + roomType, + false + ) + } + private var externalSignalingServer: ExternalSignalingServer? = null + private var webSocketClient: WebSocketInstance? = null + private var webSocketConnectionHelper: WebSocketConnectionHelper? = null + private var hasMCU = false + private var hasExternalSignalingServer = false + private var conversationPassword: String? = null + private var powerManagerUtils: PowerManagerUtils? = null + private var handler: Handler? = null + private var currentCallStatus: CallStatus? = null + private var mediaPlayer: MediaPlayer? = null + + private val participantItems = mutableStateListOf() + private var binding: CallActivityBinding? = null + private var audioOutputDialog: AudioOutputDialog? = null + private var moreCallActionsDialog: MoreCallActionsDialog? = null + private var elapsedSeconds: Long = 0 + + private var requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissionMap: Map -> + val rationaleList: MutableList = ArrayList() + val audioPermission = permissionMap[Manifest.permission.RECORD_AUDIO] + if (audioPermission != null) { + if (java.lang.Boolean.TRUE == audioPermission) { + Log.d(TAG, "Microphone permission was granted") + } else { + rationaleList.add(resources.getString(R.string.nc_microphone_permission_hint)) + } + } + val cameraPermission = permissionMap[Manifest.permission.CAMERA] + if (cameraPermission != null) { + if (java.lang.Boolean.TRUE == cameraPermission) { + Log.d(TAG, "Camera permission was granted") + } else { + rationaleList.add(resources.getString(R.string.nc_camera_permission_hint)) + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val bluetoothPermission = permissionMap[Manifest.permission.BLUETOOTH_CONNECT] + if (bluetoothPermission != null) { + if (java.lang.Boolean.TRUE == bluetoothPermission) { + enableBluetoothManager() + } else { + // Only ask for bluetooth when already asking to grant microphone or camera access. Asking + // for bluetooth solely is not important enough here and would most likely annoy the user. + if (rationaleList.isNotEmpty()) { + rationaleList.add(resources.getString(R.string.nc_bluetooth_permission_hint)) + } + } + } + } + if (rationaleList.isNotEmpty()) { + showRationaleDialogForSettings(rationaleList) + } + + if (!isConnectionEstablished) { + prepareCall() + } + } + private var canPublishAudioStream = false + private var canPublishVideoStream = false + private var isModerator = false + private var reactionAnimator: ReactionAnimator? = null + private var othersInCall = false + private var isOneToOneConversation = false + + private lateinit var micInputAudioRecorder: AudioRecord + private var micInputAudioRecordThread: Thread? = null + private var isMicInputAudioThreadRunning: Boolean = false + private val bufferSize = AudioRecord.getMinBufferSize( + SAMPLE_RATE, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + + private var recordingConsentGiven = false + + @SuppressLint("ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + Log.d(TAG, "onCreate") + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + + rootEglBase = EglBase.create() + binding = CallActivityBinding.inflate(layoutInflater) + setContentView(binding!!.root) + hideNavigationIfNoPipAvailable() + processExtras(intent.extras!!) + + conversationUser = currentUserProvider.currentUser.blockingGet() + + credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token) + if (TextUtils.isEmpty(baseUrl)) { + baseUrl = conversationUser!!.baseUrl + } + powerManagerUtils = PowerManagerUtils() + + setCallState(CallStatus.CONNECTING) + + initRaiseHandViewModel() + initCallRecordingViewModel(intent.extras!!.getInt(KEY_RECORDING_STATE)) + initClickListeners(isModerator, isOneToOneConversation) + binding!!.microphoneButton.setOnTouchListener(MicrophoneButtonTouchListener()) + pulseAnimation = PulseAnimation.create().with(binding!!.microphoneButton) + .setDuration(PULSE_ANIMATION_DURATION) + .setRepeatCount(PulseAnimation.INFINITE) + .setRepeatMode(PulseAnimation.REVERSE) + callParticipants = HashMap() + reactionAnimator = ReactionAnimator(context, binding!!.reactionAnimationWrapper, viewThemeUtils) + + checkInitialDevicePermissions() + } + + private fun initCallRecordingViewModel(recordingState: Int) { + callRecordingViewModel = ViewModelProvider(this, viewModelFactory).get( + CallRecordingViewModel::class.java + ) + callRecordingViewModel!!.setData(roomToken!!) + callRecordingViewModel!!.setRecordingState(recordingState) + callRecordingViewModel!!.viewState.observe(this) { viewState: CallRecordingViewModel.ViewState? -> + if (viewState is RecordingStartedState) { + binding!!.callRecordingIndicator.setImageResource(R.drawable.record_stop) + binding!!.callRecordingIndicator.visibility = View.VISIBLE + if (viewState.showStartedInfo) { + vibrateShort(context) + Snackbar.make( + binding!!.root, + context.resources.getString(R.string.record_active_info), + Snackbar.LENGTH_LONG + ).show() + } + } else if (viewState is RecordingStartingState) { + if (isAllowedToStartOrStopRecording) { + binding!!.callRecordingIndicator.setImageResource(R.drawable.record_starting) + binding!!.callRecordingIndicator.visibility = View.VISIBLE + } else { + binding!!.callRecordingIndicator.visibility = View.GONE + } + } else if (viewState is RecordingConfirmStopState) { + if (isAllowedToStartOrStopRecording) { + val dialogBuilder = MaterialAlertDialogBuilder(this) + .setTitle(R.string.record_stop_confirm_title) + .setMessage(R.string.record_stop_confirm_message) + .setPositiveButton(R.string.record_stop_description) { _: DialogInterface?, _: Int -> + callRecordingViewModel!!.stopRecording() + } + .setNegativeButton(R.string.nc_common_dismiss) { _: DialogInterface?, _: Int -> + callRecordingViewModel!!.dismissStopRecording() + } + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } else { + Log.e(TAG, "Being in RecordingConfirmStopState as non moderator. This should not happen!") + } + } else if (viewState is RecordingErrorState) { + if (isAllowedToStartOrStopRecording) { + Snackbar.make( + binding!!.root, + context.resources.getString(R.string.record_failed_info), + Snackbar.LENGTH_LONG + ).show() + } + binding!!.callRecordingIndicator.visibility = View.GONE + } else { + binding!!.callRecordingIndicator.visibility = View.GONE + } + } + } + + private fun initRaiseHandViewModel() { + raiseHandViewModel = ViewModelProvider(this, viewModelFactory).get(RaiseHandViewModel::class.java) + raiseHandViewModel!!.setData(roomToken!!, isBreakoutRoom) + raiseHandViewModel!!.viewState.observe(this) { viewState: RaiseHandViewModel.ViewState? -> + var raised = false + if (viewState is RaisedHandState) { + binding!!.lowerHandButton.visibility = View.VISIBLE + raised = true + } else if (viewState is LoweredHandState) { + binding!!.lowerHandButton.visibility = View.GONE + raised = false + } + if (isConnectionEstablished) { + for (peerConnectionWrapper in peerConnectionWrapperList) { + peerConnectionWrapper.raiseHand(raised) + } + } + } + } + + private fun processExtras(extras: Bundle) { + roomId = extras.getString(KEY_ROOM_ID, "") + roomToken = extras.getString(KEY_ROOM_TOKEN, "") + conversationPassword = extras.getString(KEY_CONVERSATION_PASSWORD, "") + conversationName = extras.getString(KEY_CONVERSATION_NAME, "") + isVoiceOnlyCall = extras.getBoolean(KEY_CALL_VOICE_ONLY, false) + isCallWithoutNotification = extras.getBoolean(KEY_CALL_WITHOUT_NOTIFICATION, false) + canPublishAudioStream = extras.getBoolean(KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO) + canPublishVideoStream = extras.getBoolean(KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO) + isModerator = extras.getBoolean(KEY_IS_MODERATOR, false) + isOneToOneConversation = extras.getBoolean(KEY_ROOM_ONE_TO_ONE, false) + + if (extras.containsKey(KEY_FROM_NOTIFICATION_START_CALL)) { + isIncomingCallFromNotification = extras.getBoolean(KEY_FROM_NOTIFICATION_START_CALL) + } + if (extras.containsKey(KEY_IS_BREAKOUT_ROOM)) { + isBreakoutRoom = extras.getBoolean(KEY_IS_BREAKOUT_ROOM) + } + + baseUrl = extras.getString(KEY_MODIFIED_BASE_URL, "") + } + + private fun checkRecordingConsentAndInitiateCall() { + fun askForRecordingConsent() { + val materialAlertDialogBuilder = MaterialAlertDialogBuilder(this) + .setTitle(R.string.recording_consent_title) + .setMessage(R.string.recording_consent_description) + .setCancelable(false) + .setPositiveButton(R.string.nc_yes) { _, _ -> + recordingConsentGiven = true + initiateCall() + } + .setNegativeButton(R.string.nc_no) { _, _ -> + recordingConsentGiven = false + hangup(true, false) + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, materialAlertDialogBuilder) + val dialog = materialAlertDialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + + when (CapabilitiesUtil.getRecordingConsentType(conversationUser!!.capabilities!!.spreedCapability!!)) { + CapabilitiesUtil.RECORDING_CONSENT_NOT_REQUIRED -> initiateCall() + CapabilitiesUtil.RECORDING_CONSENT_REQUIRED -> askForRecordingConsent() + CapabilitiesUtil.RECORDING_CONSENT_DEPEND_ON_CONVERSATION -> { + val getRoomApiVersion = ApiUtils.getConversationApiVersion( + conversationUser!!, + intArrayOf(ApiUtils.API_V4, 1) + ) + ncApi!!.getRoom(credentials, ApiUtils.getUrlForRoom(getRoomApiVersion, baseUrl, roomToken)) + .retry(API_RETRIES) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(roomOverall: RoomOverall) { + val conversation = roomOverall.ocs!!.data + if (conversation?.recordingConsentRequired == 1) { + askForRecordingConsent() + } else { + initiateCall() + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to get room", e) + Snackbar.make(binding!!.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + override fun onComplete() { + // unused atm + } + }) + } + } + } + + override fun onResume() { + super.onResume() + if (hasSpreedFeatureCapability( + conversationUser.capabilities!!.spreedCapability!!, + SpreedFeatures.RECORDING_V1 + ) && + othersInCall && + elapsedSeconds.toInt() >= CALL_TIME_ONE_HOUR + ) { + showCallRunningSinceOneHourOrMoreInfo() + } + } + + fun sendReaction(emoji: String?) { + addReactionForAnimation(emoji, conversationUser!!.displayName) + if (isConnectionEstablished) { + for (peerConnectionWrapper in peerConnectionWrapperList) { + peerConnectionWrapper.sendReaction(emoji) + } + } + } + + override fun onStart() { + super.onStart() + active = true + initFeaturesVisibility() + try { + cache!!.evictAll() + } catch (e: IOException) { + Log.e(TAG, "Failed to evict cache") + } + } + + override fun onStop() { + super.onStop() + active = false + + if (isMicInputAudioThreadRunning) { + stopMicInputDetection() + } + } + + private fun stopMicInputDetection() { + if (micInputAudioRecordThread != null) { + micInputAudioRecorder.stop() + micInputAudioRecorder.release() + isMicInputAudioThreadRunning = false + micInputAudioRecordThread = null + } + } + + private fun enableBluetoothManager() { + if (audioManager != null) { + audioManager!!.startBluetoothManager() + } + } + + private fun initFeaturesVisibility() { + if (isAllowedToStartOrStopRecording || isAllowedToRaiseHand) { + binding!!.moreCallActions.visibility = View.VISIBLE + } else { + binding!!.moreCallActions.visibility = View.GONE + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun initClickListeners(isModerator: Boolean, isOneToOneConversation: Boolean) { + initCallActionClickListeners(isModerator, isOneToOneConversation) + + if (canPublishAudioStream) { + binding!!.microphoneButton.setOnClickListener { onMicrophoneClick() } + binding!!.microphoneButton.setOnLongClickListener { + if (!microphoneOn) { + callControlHandler.removeCallbacksAndMessages(null) + callInfosHandler.removeCallbacksAndMessages(null) + cameraSwitchHandler.removeCallbacksAndMessages(null) + isPushToTalkActive = true + binding!!.callControls.visibility = View.VISIBLE + if (!isVoiceOnlyCall) { + binding!!.switchSelfVideoButton.visibility = View.VISIBLE + } + } + onMicrophoneClick() + true + } + } else { + binding!!.microphoneButton.setOnClickListener { + Snackbar.make(binding!!.root, R.string.nc_not_allowed_to_activate_audio, Snackbar.LENGTH_SHORT).show() + } + } + + if (canPublishVideoStream) { + binding!!.cameraButton.setOnClickListener { onCameraClick() } + } else { + binding!!.cameraButton.setOnClickListener { + Snackbar.make(binding!!.root, R.string.nc_not_allowed_to_activate_video, Snackbar.LENGTH_SHORT).show() + } + } + + binding!!.callStates.callStateRelativeLayout.setOnClickListener { + if (currentCallStatus === CallStatus.CALLING_TIMEOUT) { + setCallState(CallStatus.RECONNECTING) + hangupNetworkCalls(shutDownView = false, endCallForAll = false) + } + } + binding!!.callRecordingIndicator.setOnClickListener { + if (isAllowedToStartOrStopRecording) { + if (callRecordingViewModel!!.viewState.value is RecordingStartingState) { + if (moreCallActionsDialog == null) { + moreCallActionsDialog = MoreCallActionsDialog(this) + } + moreCallActionsDialog!!.show() + } else { + callRecordingViewModel!!.clickRecordButton() + } + } else { + Snackbar.make( + binding!!.root, + context.resources.getString(R.string.record_active_info), + Snackbar.LENGTH_LONG + ).show() + } + } + } + + private fun initCallActionClickListeners(isModerator: Boolean, isOneToOneConversation: Boolean) { + binding!!.audioOutputButton.setOnClickListener { + audioOutputDialog = AudioOutputDialog(this) + audioOutputDialog!!.show() + } + + binding!!.moreCallActions.setOnClickListener { + moreCallActionsDialog = MoreCallActionsDialog(this) + moreCallActionsDialog!!.show() + } + + if (isOneToOneConversation) { + binding!!.hangupButton.setOnLongClickListener { + showLeaveCallPopupMenu() + true + } + binding!!.hangupButton.setOnClickListener { + hangup(shutDownView = true, endCallForAll = true) + } + binding!!.endCallPopupMenu.setOnClickListener { + hangup(shutDownView = true, endCallForAll = true) + binding!!.endCallPopupMenu.visibility = View.GONE + } + } else { + if (isModerator) { + binding!!.hangupButton.setOnLongClickListener { + showEndCallForAllPopupMenu() + true + } + } + binding!!.hangupButton.setOnClickListener { + hangup(shutDownView = true, endCallForAll = false) + } + binding!!.endCallPopupMenu.setOnClickListener { + hangup(shutDownView = true, endCallForAll = false) + binding!!.endCallPopupMenu.visibility = View.GONE + } + } + + binding!!.switchSelfVideoButton.setOnClickListener { switchCamera() } + + binding!!.lowerHandButton.setOnClickListener { l: View? -> raiseHandViewModel!!.lowerHand() } + binding!!.pictureInPictureButton.setOnClickListener { enterPipMode() } + } + + private fun showEndCallForAllPopupMenu() { + binding!!.endCallPopupMenu.visibility = View.VISIBLE + binding!!.endCallPopupMenu.text = context.getString(R.string.end_call_for_everyone) + } + + private fun showLeaveCallPopupMenu() { + binding!!.endCallPopupMenu.visibility = View.VISIBLE + binding!!.endCallPopupMenu.text = context.getString(R.string.leave_call) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun createCameraEnumerator() { + var camera2EnumeratorIsSupported = false + try { + camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(this) + } catch (t: Throwable) { + Log.w(TAG, "Camera2Enumerator threw an error", t) + } + cameraEnumerator = if (camera2EnumeratorIsSupported) { + Camera2Enumerator(this) + } else { + Camera1Enumerator(WebRTCUtils.shouldEnableVideoHardwareAcceleration()) + } + } + + private fun basicInitialization() { + createCameraEnumerator() + + // Create a new PeerConnectionFactory instance. + val options = PeerConnectionFactory.Options() + val defaultVideoEncoderFactory = DefaultVideoEncoderFactory( + rootEglBase!!.eglBaseContext, + true, + true + ) + val defaultVideoDecoderFactory = DefaultVideoDecoderFactory( + rootEglBase!!.eglBaseContext + ) + peerConnectionFactory = PeerConnectionFactory.builder() + .setOptions(options) + .setVideoEncoderFactory(defaultVideoEncoderFactory) + .setVideoDecoderFactory(defaultVideoDecoderFactory) + .createPeerConnectionFactory() + + // Create MediaConstraints - Will be useful for specifying video and audio constraints. + audioConstraints = MediaConstraints() + videoConstraints = MediaConstraints() + localStream = peerConnectionFactory!!.createLocalMediaStream("NCMS") + + // Create and audio manager that will take care of audio routing, + // audio modes, audio device enumeration etc. + audioManager = WebRtcAudioManager.create(applicationContext, isVoiceOnlyCall) + // Store existing audio settings and change audio mode to + // MODE_IN_COMMUNICATION for best possible VoIP performance. + Log.d(TAG, "Starting the audio manager...") + audioManager!!.start { currentDevice: AudioDevice, availableDevices: Set -> + onAudioManagerDevicesChanged( + currentDevice, + availableDevices + ) + } + if (isVoiceOnlyCall) { + setDefaultAudioOutputChannel(AudioDevice.EARPIECE) + } else { + setDefaultAudioOutputChannel(AudioDevice.SPEAKER_PHONE) + } + iceServers = ArrayList() + + // create sdpConstraints + sdpConstraints = MediaConstraints() + sdpConstraintsForMCUPublisher = MediaConstraints() + sdpConstraints!!.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) + var offerToReceiveVideoString = "true" + if (isVoiceOnlyCall) { + offerToReceiveVideoString = "false" + } + sdpConstraints!!.mandatory.add( + MediaConstraints.KeyValuePair("OfferToReceiveVideo", offerToReceiveVideoString) + ) + sdpConstraintsForMCUPublisher!!.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false")) + sdpConstraintsForMCUPublisher!!.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")) + sdpConstraintsForMCUPublisher!!.optional.add(MediaConstraints.KeyValuePair("internalSctpDataChannels", "true")) + sdpConstraintsForMCUPublisher!!.optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")) + sdpConstraints!!.optional.add(MediaConstraints.KeyValuePair("internalSctpDataChannels", "true")) + sdpConstraints!!.optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")) + if (!isVoiceOnlyCall) { + cameraInitialization() + } + microphoneInitialization() + } + + fun setDefaultAudioOutputChannel(selectedAudioDevice: AudioDevice?) { + if (audioManager != null) { + audioManager!!.setDefaultAudioDevice(selectedAudioDevice) + updateAudioOutputButton(audioManager!!.currentAudioDevice) + } + } + + fun setAudioOutputChannel(selectedAudioDevice: AudioDevice?) { + if (audioManager != null) { + audioManager!!.selectAudioDevice(selectedAudioDevice) + updateAudioOutputButton(audioManager!!.currentAudioDevice) + } + } + + private fun updateAudioOutputButton(activeAudioDevice: AudioDevice) { + when (activeAudioDevice) { + AudioDevice.BLUETOOTH -> binding!!.audioOutputButton.setImageResource( + R.drawable.ic_baseline_bluetooth_audio_24 + ) + + AudioDevice.SPEAKER_PHONE -> binding!!.audioOutputButton.setImageResource( + R.drawable.ic_volume_up_white_24dp + ) + + AudioDevice.EARPIECE -> binding!!.audioOutputButton.setImageResource( + R.drawable.ic_baseline_phone_in_talk_24 + ) + + AudioDevice.WIRED_HEADSET -> binding!!.audioOutputButton.setImageResource( + R.drawable.ic_baseline_headset_mic_24 + ) + + else -> Log.e(TAG, "Icon for audio output not available") + } + DrawableCompat.setTint(binding!!.audioOutputButton.drawable, Color.WHITE) + } + + @SuppressLint("ClickableViewAccessibility") + private fun initViews() { + Log.d(TAG, "initViews") + binding!!.callInfosLinearLayout.visibility = View.VISIBLE + if (!isPipModePossible) { + binding!!.pictureInPictureButton.visibility = View.GONE + } + if (isVoiceOnlyCall) { + binding!!.switchSelfVideoButton.visibility = View.GONE + binding!!.cameraButton.visibility = View.GONE + binding!!.selfVideoRenderer.visibility = View.GONE + val params = RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.addRule(RelativeLayout.BELOW, R.id.callInfosLinearLayout) + val callControlsHeight = + applicationContext.resources.getDimension(R.dimen.call_controls_height).roundToInt() + params.setMargins(0, 0, 0, callControlsHeight) + binding!!.composeParticipantGrid.layoutParams = params + } else { + val params = RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.setMargins(0, 0, 0, 0) + binding!!.composeParticipantGrid.layoutParams = params + if (cameraEnumerator!!.deviceNames.size < 2) { + binding!!.switchSelfVideoButton.visibility = View.GONE + } + initSelfVideoViewForNormalMode() + } + binding!!.composeParticipantGrid.setOnTouchListener { _, me -> + val action = me.actionMasked + if (action == MotionEvent.ACTION_DOWN) { + animateCallControls(true, 0) + binding!!.endCallPopupMenu.visibility = View.GONE + } + false + } + binding!!.conversationRelativeLayout.setOnTouchListener { _, me -> + val action = me.actionMasked + if (action == MotionEvent.ACTION_DOWN) { + animateCallControls(true, 0) + binding!!.endCallPopupMenu.visibility = View.GONE + } + false + } + animateCallControls(true, 0) + initGrid() + binding!!.composeParticipantGrid.z = 0f + } + + @SuppressLint("ClickableViewAccessibility") + private fun initSelfVideoViewForNormalMode() { + try { + binding!!.selfVideoRenderer.init(rootEglBase!!.eglBaseContext, null) + } catch (e: IllegalStateException) { + Log.d(TAG, "selfVideoRenderer already initialized", e) + } + binding!!.selfVideoRenderer.setZOrderMediaOverlay(true) + // disabled because it causes some devices to crash + binding!!.selfVideoRenderer.setEnableHardwareScaler(false) + binding!!.selfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) + binding!!.selfVideoRenderer.setOnTouchListener(SelfVideoTouchListener()) + + binding!!.pipSelfVideoRenderer.clearImage() + binding!!.pipSelfVideoRenderer.release() + } + + private fun initGrid() { + Log.d(TAG, "initGrid") + binding!!.composeParticipantGrid.visibility = View.VISIBLE + binding!!.composeParticipantGrid.setContent { + MaterialTheme { + val participantUiStates = participantItems.map { it.uiStateFlow.collectAsState().value } + ParticipantGrid( + participantUiStates = participantUiStates, + eglBase = rootEglBase!!, + isVoiceOnlyCall = isVoiceOnlyCall + ) { + animateCallControls(true, 0) + } + } + } + + if (isInPipMode) { + updateUiForPipMode() + } + } + + private fun checkInitialDevicePermissions() { + val permissionsToRequest: MutableList = ArrayList() + val rationaleList: MutableList = ArrayList() + if (permissionUtil!!.isMicrophonePermissionGranted()) { + Log.d(TAG, "Microphone permission already granted") + } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) { + permissionsToRequest.add(Manifest.permission.RECORD_AUDIO) + rationaleList.add(resources.getString(R.string.nc_microphone_permission_hint)) + } else { + permissionsToRequest.add(Manifest.permission.RECORD_AUDIO) + } + + if (!isVoiceOnlyCall) { + if (permissionUtil!!.isCameraPermissionGranted()) { + Log.d(TAG, "Camera permission already granted") + } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + permissionsToRequest.add(Manifest.permission.CAMERA) + rationaleList.add(resources.getString(R.string.nc_camera_permission_hint)) + } else { + permissionsToRequest.add(Manifest.permission.CAMERA) + } + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (permissionUtil!!.isBluetoothPermissionGranted()) { + enableBluetoothManager() + } else if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) { + permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT) + rationaleList.add(resources.getString(R.string.nc_bluetooth_permission_hint)) + } else { + permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT) + } + } + + if (permissionsToRequest.isNotEmpty()) { + if (rationaleList.isNotEmpty()) { + showRationaleDialog(permissionsToRequest, rationaleList) + } else { + requestPermissionLauncher.launch(permissionsToRequest.toTypedArray()) + } + } else if (!isConnectionEstablished) { + prepareCall() + } + } + + private fun prepareCall() { + basicInitialization() + initViews() + updateSelfVideoViewPosition(true) + checkRecordingConsentAndInitiateCall() + + if (permissionUtil!!.isMicrophonePermissionGranted()) { + if (!microphoneOn) { + onMicrophoneClick() + } + } + + if (isVoiceOnlyCall) { + binding!!.selfVideoViewWrapper.visibility = View.GONE + } else if (permissionUtil!!.isCameraPermissionGranted()) { + binding!!.selfVideoViewWrapper.visibility = View.VISIBLE + onCameraClick() + if (cameraEnumerator!!.deviceNames.isEmpty()) { + binding!!.cameraButton.visibility = View.GONE + } + if (cameraEnumerator!!.deviceNames.size > 1) { + binding!!.switchSelfVideoButton.visibility = View.VISIBLE + } + } + } + + private fun showRationaleDialog(permissionToRequest: String, rationale: String) { + val rationaleList: MutableList = ArrayList() + val permissionsToRequest: MutableList = ArrayList() + rationaleList.add(rationale) + permissionsToRequest.add(permissionToRequest) + showRationaleDialog(permissionsToRequest, rationaleList) + } + + private fun showRationaleDialog(permissionsToRequest: List, rationaleList: List) { + val rationalesWithLineBreaks = StringBuilder() + for (rationale in rationaleList) { + rationalesWithLineBreaks.append(rationale).append("\n\n") + } + val dialogBuilder = MaterialAlertDialogBuilder(this) + .setTitle(R.string.nc_permissions_rationale_dialog_title) + .setMessage(rationalesWithLineBreaks) + .setPositiveButton(R.string.nc_permissions_ask) { _, _ -> + requestPermissionLauncher.launch(permissionsToRequest.toTypedArray()) + } + .setNegativeButton(R.string.nc_common_dismiss, null) + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder) + dialogBuilder.show() + } + + private fun showRationaleDialogForSettings(rationaleList: List) { + val rationalesWithLineBreaks = StringBuilder() + rationalesWithLineBreaks.append(resources.getString(R.string.nc_permissions_denied)) + rationalesWithLineBreaks.append('\n') + rationalesWithLineBreaks.append(resources.getString(R.string.nc_permissions_settings_hint)) + rationalesWithLineBreaks.append("\n\n") + for (rationale in rationaleList) { + rationalesWithLineBreaks.append(rationale).append("\n\n") + } + val dialogBuilder = MaterialAlertDialogBuilder(this) + .setTitle(R.string.nc_permissions_rationale_dialog_title) + .setMessage(rationalesWithLineBreaks) + .setPositiveButton(R.string.nc_permissions_settings) { _, _ -> + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.data = Uri.fromParts("package", packageName, null) + startActivity(intent) + } + .setNegativeButton(R.string.nc_common_dismiss, null) + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder) + dialogBuilder.show() + } + + private val isConnectionEstablished: Boolean + get() = currentCallStatus === CallStatus.JOINED || currentCallStatus === CallStatus.IN_CONVERSATION + + private fun onAudioManagerDevicesChanged(currentDevice: AudioDevice, availableDevices: Set) { + Log.d(TAG, "onAudioManagerDevicesChanged: $availableDevices, currentDevice: $currentDevice") + val shouldDisableProximityLock = + currentDevice == AudioDevice.WIRED_HEADSET || + currentDevice == AudioDevice.SPEAKER_PHONE || + currentDevice == AudioDevice.BLUETOOTH + if (shouldDisableProximityLock) { + powerManagerUtils!!.updatePhoneState(PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK) + } else { + powerManagerUtils!!.updatePhoneState(PowerManagerUtils.PhoneState.WITH_PROXIMITY_SENSOR_LOCK) + } + if (audioOutputDialog != null) { + audioOutputDialog!!.updateOutputDeviceList() + } + updateAudioOutputButton(currentDevice) + } + + private fun cameraInitialization() { + videoCapturer = createCameraCapturer(cameraEnumerator) + + // Create a VideoSource instance + if (videoCapturer != null) { + val surfaceTextureHelper = SurfaceTextureHelper.create( + "CaptureThread", + rootEglBase!!.eglBaseContext + ) + videoSource = peerConnectionFactory!!.createVideoSource(false) + videoCapturer!!.initialize(surfaceTextureHelper, applicationContext, videoSource!!.capturerObserver) + } + localVideoTrack = peerConnectionFactory!!.createVideoTrack("NCv0", videoSource) + localStream!!.addTrack(localVideoTrack) + localVideoTrack!!.setEnabled(false) + localVideoTrack!!.addSink(binding!!.selfVideoRenderer) + localCallParticipantModel.isVideoEnabled = false + } + + private fun microphoneInitialization() { + startMicInputDetection() + + // create an AudioSource instance + audioSource = peerConnectionFactory!!.createAudioSource(audioConstraints) + localAudioTrack = peerConnectionFactory!!.createAudioTrack("NCa0", audioSource) + localAudioTrack!!.setEnabled(false) + localStream!!.addTrack(localAudioTrack) + localCallParticipantModel.isAudioEnabled = false + } + + @SuppressLint("MissingPermission") + private fun startMicInputDetection() { + if (permissionUtil!!.isMicrophonePermissionGranted() && micInputAudioRecordThread == null) { + micInputAudioRecorder = AudioRecord( + MediaRecorder.AudioSource.MIC, + SAMPLE_RATE, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + bufferSize + ) + isMicInputAudioThreadRunning = true + micInputAudioRecorder.startRecording() + micInputAudioRecordThread = Thread( + Runnable { + while (isMicInputAudioThreadRunning) { + val byteArr = ByteArray(bufferSize / 2) + micInputAudioRecorder.read(byteArr, 0, byteArr.size) + val isCurrentlySpeaking = abs(byteArr[0].toDouble()) > MICROPHONE_VALUE_THRESHOLD + + localCallParticipantModel.isSpeaking = isCurrentlySpeaking + + Thread.sleep(MICROPHONE_VALUE_SLEEP) + } + } + ) + micInputAudioRecordThread!!.start() + } + } + + private fun createCameraCapturer(enumerator: CameraEnumerator?): VideoCapturer? { + val deviceNames = enumerator!!.deviceNames + + // First, try to find front facing camera + Logging.d(TAG, "Looking for front facing cameras.") + for (deviceName in deviceNames) { + if (enumerator.isFrontFacing(deviceName)) { + Logging.d(TAG, "Creating front facing camera capturer.") + val videoCapturer: VideoCapturer? = enumerator.createCapturer(deviceName, null) + if (videoCapturer != null) { + binding!!.selfVideoRenderer.setMirror(true) + return videoCapturer + } + } + } + + // Front facing camera not found, try something else + Logging.d(TAG, "Looking for other cameras.") + for (deviceName in deviceNames) { + if (!enumerator.isFrontFacing(deviceName)) { + Logging.d(TAG, "Creating other camera capturer.") + val videoCapturer: VideoCapturer? = enumerator.createCapturer(deviceName, null) + if (videoCapturer != null) { + binding!!.selfVideoRenderer.setMirror(false) + return videoCapturer + } + } + } + return null + } + + fun onMicrophoneClick() { + if (!canPublishAudioStream) { + microphoneOn = false + binding!!.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px) + toggleMedia(false, false) + } + if (!canPublishAudioStream) { + // In the case no audio stream will be published it's not needed to check microphone permissions + return + } + if (permissionUtil!!.isMicrophonePermissionGranted()) { + if (!appPreferences.pushToTalkIntroShown) { + spotlightView = getSpotlightView() + appPreferences.pushToTalkIntroShown = true + } + if (!isPushToTalkActive) { + microphoneOn = !microphoneOn + if (microphoneOn) { + binding!!.microphoneButton.setImageResource(R.drawable.ic_mic_white_24px) + updatePictureInPictureActions( + R.drawable.ic_mic_white_24px, + resources.getString(R.string.nc_pip_microphone_mute), + MICROPHONE_PIP_REQUEST_MUTE + ) + } else { + binding!!.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px) + updatePictureInPictureActions( + R.drawable.ic_mic_off_white_24px, + resources.getString(R.string.nc_pip_microphone_unmute), + MICROPHONE_PIP_REQUEST_UNMUTE + ) + } + toggleMedia(microphoneOn, false) + } else { + binding!!.microphoneButton.setImageResource(R.drawable.ic_mic_white_24px) + pulseAnimation!!.start() + toggleMedia(true, false) + } + } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) { + showRationaleDialog( + Manifest.permission.RECORD_AUDIO, + resources.getString(R.string.nc_microphone_permission_hint) + ) + } else { + requestPermissionLauncher.launch(PERMISSIONS_MICROPHONE) + } + } + + private fun getSpotlightView(): SpotlightView? { + val builder = SpotlightView.Builder(this) + .introAnimationDuration(INTRO_ANIMATION_DURATION) + .enableRevealAnimation(true) + .performClick(false) + .fadeinTextDuration(FADE_IN_ANIMATION_DURATION) + .headingTvSize(SPOTLIGHT_HEADING_SIZE) + .headingTvText(resources.getString(R.string.nc_push_to_talk)) + .subHeadingTvColor(resources.getColor(R.color.bg_default, null)) + .subHeadingTvSize(SPOTLIGHT_SUBHEADING_SIZE) + .subHeadingTvText(resources.getString(R.string.nc_push_to_talk_desc)) + .maskColor("#dc000000".toColorInt()) + .target(binding!!.microphoneButton) + .lineAnimDuration(FADE_IN_ANIMATION_DURATION) + .enableDismissAfterShown(true) + .dismissOnBackPress(true) + .usageId("pushToTalk") + + return viewThemeUtils.talk.themeSpotlightView(context, builder).show() + } + + private fun onCameraClick() { + if (!canPublishVideoStream) { + videoOn = false + binding!!.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px) + binding!!.switchSelfVideoButton.visibility = View.GONE + return + } + if (permissionUtil!!.isCameraPermissionGranted()) { + videoOn = !videoOn + if (videoOn) { + binding!!.cameraButton.setImageResource(R.drawable.ic_videocam_white_24px) + if (cameraEnumerator!!.deviceNames.size > 1) { + binding!!.switchSelfVideoButton.visibility = View.VISIBLE + } + } else { + binding!!.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px) + binding!!.switchSelfVideoButton.visibility = View.GONE + } + toggleMedia(videoOn, true) + } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + showRationaleDialog( + Manifest.permission.CAMERA, + resources.getString(R.string.nc_camera_permission_hint) + ) + } else { + requestPermissionLauncher.launch(PERMISSIONS_CAMERA) + } + } + + fun switchCamera() { + val cameraVideoCapturer = videoCapturer as CameraVideoCapturer? + cameraVideoCapturer?.switchCamera(object : CameraSwitchHandler { + override fun onCameraSwitchDone(currentCameraIsFront: Boolean) { + binding!!.selfVideoRenderer.setMirror(currentCameraIsFront) + } + + override fun onCameraSwitchError(s: String) { + Log.e(TAG, "Error while switching camera: $s") + } + }) + } + + private fun toggleMedia(enable: Boolean, video: Boolean) { + if (video) { + if (enable) { + binding!!.cameraButton.alpha = OPACITY_ENABLED + startVideoCapture( + isPortrait = true + ) + setupOrientationListener(context) + } else { + binding!!.cameraButton.alpha = OPACITY_DISABLED + if (videoCapturer != null) { + try { + videoCapturer!!.stopCapture() + } catch (e: InterruptedException) { + Log.d(TAG, "Failed to stop capturing video while sensor is near the ear") + } + } + } + if (localStream != null && localStream!!.videoTracks.size > 0) { + localStream!!.videoTracks[0].setEnabled(enable) + localCallParticipantModel.isVideoEnabled = enable + } + if (enable) { + binding!!.selfVideoRenderer.visibility = View.VISIBLE + binding!!.pipSelfVideoRenderer.visibility = View.VISIBLE + + initSelfVideoViewForNormalMode() + } else { + binding!!.selfVideoRenderer.visibility = View.INVISIBLE + binding!!.pipSelfVideoRenderer.visibility = View.INVISIBLE + + binding!!.selfVideoRenderer.clearImage() + binding!!.selfVideoRenderer.release() + + binding!!.pipSelfVideoRenderer.clearImage() + binding!!.pipSelfVideoRenderer.release() + } + } else { + if (enable) { + binding!!.microphoneButton.alpha = OPACITY_ENABLED + } else { + binding!!.microphoneButton.alpha = OPACITY_DISABLED + } + if (localStream != null && localStream!!.audioTracks.size > 0) { + localStream!!.audioTracks[0].setEnabled(enable) + localCallParticipantModel.isAudioEnabled = enable + } + } + } + + fun clickRaiseOrLowerHandButton() { + raiseHandViewModel!!.clickHandButton() + } + + private fun animateCallControls(show: Boolean, startDelay: Long) { + if (isVoiceOnlyCall) { + if (spotlightView != null && spotlightView!!.visibility != View.GONE) { + spotlightView!!.visibility = View.GONE + } + } else if (!isPushToTalkActive) { + val alpha: Float + val duration: Long + if (show) { + callControlHandler.removeCallbacksAndMessages(null) + callInfosHandler.removeCallbacksAndMessages(null) + cameraSwitchHandler.removeCallbacksAndMessages(null) + alpha = OPACITY_ENABLED + duration = SECOND_IN_MILLIS + if (binding!!.callControls.visibility != View.VISIBLE) { + binding!!.callControls.alpha = OPACITY_INVISIBLE + binding!!.callControls.visibility = View.VISIBLE + binding!!.callInfosLinearLayout.alpha = OPACITY_INVISIBLE + binding!!.callInfosLinearLayout.visibility = View.VISIBLE + binding!!.switchSelfVideoButton.alpha = OPACITY_INVISIBLE + if (videoOn) { + binding!!.switchSelfVideoButton.visibility = View.VISIBLE + } + } else { + callControlHandler.postDelayed({ animateCallControls(false, 0) }, FIVE_SECONDS) + return + } + } else { + alpha = OPACITY_INVISIBLE + duration = SECOND_IN_MILLIS + } + binding!!.callControls.isEnabled = false + binding!!.callControls.animate() + .translationY(0f) + .alpha(alpha) + .setDuration(duration) + .setStartDelay(startDelay) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + if (!show) { + binding!!.callControls.visibility = View.GONE + if (spotlightView != null && spotlightView!!.visibility != View.GONE) { + spotlightView!!.visibility = View.GONE + } + } else { + callControlHandler.postDelayed({ + if (!isPushToTalkActive) { + animateCallControls(false, 0) + } + }, CALL_CONTROLLS_ANIMATION_DELAY) + } + binding!!.callControls.isEnabled = true + } + }) + binding!!.callInfosLinearLayout.isEnabled = false + binding!!.callInfosLinearLayout.animate() + .translationY(0f) + .alpha(alpha) + .setDuration(duration) + .setStartDelay(startDelay) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + if (!show) { + binding!!.callInfosLinearLayout.visibility = View.GONE + } else { + callInfosHandler.postDelayed({ + if (!isPushToTalkActive) { + animateCallControls(false, 0) + } + }, CALL_CONTROLLS_ANIMATION_DELAY) + } + binding!!.callInfosLinearLayout.isEnabled = true + } + }) + binding!!.switchSelfVideoButton.isEnabled = false + binding!!.switchSelfVideoButton.animate() + .translationY(0f) + .alpha(alpha) + .setDuration(duration) + .setStartDelay(startDelay) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + if (!show) { + binding!!.switchSelfVideoButton.visibility = View.GONE + } + binding!!.switchSelfVideoButton.isEnabled = true + } + }) + } + } + + public override fun onDestroy() { + if (signalingMessageReceiver != null) { + signalingMessageReceiver!!.removeListener(localParticipantMessageListener) + signalingMessageReceiver!!.removeListener(offerMessageListener) + } + if (localStream != null) { + localStream!!.dispose() + localStream = null + Log.d(TAG, "Disposed localStream") + } else { + Log.d(TAG, "localStream is null") + } + if (currentCallStatus !== CallStatus.LEAVING) { + hangup(true, false) + } + powerManagerUtils!!.updatePhoneState(PowerManagerUtils.PhoneState.IDLE) + super.onDestroy() + } + + private fun fetchSignalingSettings() { + Log.d(TAG, "fetchSignalingSettings") + val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.API_V3, 2, 1)) + ncApi!!.getSignalingSettings(credentials, ApiUtils.getUrlForSignalingSettings(apiVersion, baseUrl, roomToken!!)) + .subscribeOn(Schedulers.io()) + .retry(API_RETRIES) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(signalingSettingsOverall: SignalingSettingsOverall) { + if (signalingSettingsOverall.ocs != null && + signalingSettingsOverall.ocs!!.settings != null + ) { + externalSignalingServer = ExternalSignalingServer() + if (!TextUtils.isEmpty(signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer) && + !TextUtils.isEmpty(signalingSettingsOverall.ocs!!.settings!!.externalSignalingTicket) + ) { + externalSignalingServer = ExternalSignalingServer() + externalSignalingServer!!.externalSignalingServer = + signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer + externalSignalingServer!!.externalSignalingTicket = + signalingSettingsOverall.ocs!!.settings!!.externalSignalingTicket + externalSignalingServer!!.federation = + signalingSettingsOverall.ocs!!.settings!!.federation + hasExternalSignalingServer = true + } else { + hasExternalSignalingServer = false + } + Log.d(TAG, " hasExternalSignalingServer: $hasExternalSignalingServer") + + if ("?" != conversationUser!!.userId && conversationUser!!.id != null) { + Log.d( + TAG, + "Update externalSignalingServer for: " + conversationUser!!.id + + " / " + conversationUser!!.userId + ) + userManager!!.updateExternalSignalingServer( + conversationUser!!.id!!, + externalSignalingServer!! + ) + .subscribeOn(Schedulers.io()) + .subscribe() + } else { + conversationUser!!.externalSignalingServer = externalSignalingServer + } + + addIceServers(signalingSettingsOverall, apiVersion) + } + checkCapabilities() + } + + override fun onError(e: Throwable) { + Log.e(TAG, e.message, e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun addIceServers(signalingSettingsOverall: SignalingSettingsOverall, apiVersion: Int) { + if (signalingSettingsOverall.ocs!!.settings!!.stunServers != null) { + val stunServers = signalingSettingsOverall.ocs!!.settings!!.stunServers + if (apiVersion == ApiUtils.API_V3) { + for ((_, urls) in stunServers!!) { + if (urls != null) { + for (url in urls) { + Log.d(TAG, " STUN server url: $url") + iceServers!!.add(PeerConnection.IceServer(url)) + } + } + } + } else { + if (signalingSettingsOverall.ocs!!.settings!!.stunServers != null) { + for ((url) in stunServers!!) { + Log.d(TAG, " STUN server url: $url") + iceServers!!.add(PeerConnection.IceServer(url)) + } + } + } + } + + if (signalingSettingsOverall.ocs!!.settings!!.turnServers != null) { + val turnServers = signalingSettingsOverall.ocs!!.settings!!.turnServers + for ((_, urls, username, credential) in turnServers!!) { + if (urls != null) { + for (url in urls) { + Log.d(TAG, " TURN server url: $url") + iceServers!!.add(PeerConnection.IceServer(url, username, credential)) + } + } + } + } + } + + private fun checkCapabilities() { + ncApi!!.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl!!)) + .retry(API_RETRIES) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(capabilitiesOverall: CapabilitiesOverall) { + // FIXME check for compatible Call API version + if (hasExternalSignalingServer) { + setupAndInitiateWebSocketsConnection() + } else { + signalingMessageReceiver = internalSignalingMessageReceiver + signalingMessageReceiver!!.addListener(localParticipantMessageListener) + signalingMessageReceiver!!.addListener(offerMessageListener) + signalingMessageSender = internalSignalingMessageSender + + hasMCU = false + + messageSender = MessageSenderNoMcu( + signalingMessageSender, + callParticipants.keys, + peerConnectionWrapperList + ) + + joinRoomAndCall() + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to fetch capabilities", e) + Snackbar.make(binding!!.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + // unused atm + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun joinRoomAndCall() { + callSession = ApplicationWideCurrentRoomHolder.getInstance().session + val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1)) + Log.d(TAG, "joinRoomAndCall") + Log.d(TAG, " baseUrl= $baseUrl") + Log.d(TAG, " roomToken= $roomToken") + Log.d(TAG, " callSession= $callSession") + val url = ApiUtils.getUrlForParticipantsActive(apiVersion, baseUrl, roomToken) + Log.d(TAG, " url= $url") + + // if session is empty, e.g. we when we got here by notification, we need to join the room to get a session + if (TextUtils.isEmpty(callSession)) { + ncApi!!.joinRoom(credentials, url, conversationPassword) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retry(API_RETRIES) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(roomOverall: RoomOverall) { + val conversation = roomOverall.ocs!!.data + callRecordingViewModel!!.setRecordingState(conversation!!.callRecording) + callSession = conversation.sessionId + Log.d(TAG, " new callSession by joinRoom= $callSession") + + setInitialApplicationWideCurrentRoomHolderValues(conversation) + + callOrJoinRoomViaWebSocket() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "joinRoom onError", e) + } + + override fun onComplete() { + Log.d(TAG, "joinRoom onComplete") + } + }) + } else { + // we are in a room and start a call -> same session needs to be used + callOrJoinRoomViaWebSocket() + } + } + + private fun callOrJoinRoomViaWebSocket() { + if (hasExternalSignalingServer) { + webSocketClient!!.joinRoomWithRoomTokenAndSession( + roomToken!!, + callSession, + externalSignalingServer!!.federation + ) + } else { + performCall() + } + } + + private fun performCall() { + fun getRoomAndContinue() { + val getRoomApiVersion = ApiUtils.getConversationApiVersion( + conversationUser, + intArrayOf(ApiUtils.API_V4, 1) + ) + ncApi!!.getRoom(credentials, ApiUtils.getUrlForRoom(getRoomApiVersion, baseUrl, roomToken)) + .retry(API_RETRIES) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(roomOverall: RoomOverall) { + val conversation = roomOverall.ocs!!.data + callRecordingViewModel!!.setRecordingState(conversation!!.callRecording) + callSession = conversation.sessionId + + setInitialApplicationWideCurrentRoomHolderValues(conversation) + + startCallTimeCounter(conversation.callStartTime) + + if (currentCallStatus !== CallStatus.LEAVING) { + if (currentCallStatus !== CallStatus.IN_CONVERSATION) { + setCallState(CallStatus.JOINED) + } + ApplicationWideCurrentRoomHolder.getInstance().isInCall = true + ApplicationWideCurrentRoomHolder.getInstance().isDialing = false + if (!TextUtils.isEmpty(roomToken)) { + cancelExistingNotificationsForRoom( + applicationContext, + conversationUser!!, + roomToken!! + ) + } + if (!hasExternalSignalingServer) { + pullSignalingMessages() + } + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to get room", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + var inCallFlag = Participant.InCallFlags.IN_CALL + if (canPublishAudioStream) { + inCallFlag += Participant.InCallFlags.WITH_AUDIO + } + if (!isVoiceOnlyCall && canPublishVideoStream) { + inCallFlag += Participant.InCallFlags.WITH_VIDEO + } + callParticipantList = CallParticipantList(signalingMessageReceiver) + callParticipantList!!.addObserver(callParticipantListObserver) + + if (hasMCU) { + localStateBroadcaster = LocalStateBroadcasterMcu(localCallParticipantModel, messageSender) + } else { + localStateBroadcaster = LocalStateBroadcasterNoMcu( + localCallParticipantModel, + messageSender as MessageSenderNoMcu + ) + } + + val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1)) + ncApi!!.joinCall( + credentials, + ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken!!), + inCallFlag, + isCallWithoutNotification, + recordingConsentGiven + ) + .subscribeOn(Schedulers.io()) + .retry(API_RETRIES) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + getRoomAndContinue() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to join call", e) + Snackbar.make(binding!!.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + hangup(true, false) + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun setInitialApplicationWideCurrentRoomHolderValues(conversation: Conversation) { + ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser + ApplicationWideCurrentRoomHolder.getInstance().session = conversation.sessionId + // ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = conversation.roomId + ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = conversation.token + ApplicationWideCurrentRoomHolder.getInstance().callStartTime = conversation.callStartTime + } + + private fun startCallTimeCounter(callStartTime: Long) { + if (callStartTime != 0L && + hasSpreedFeatureCapability( + conversationUser!!.capabilities!!.spreedCapability!!, + SpreedFeatures.RECORDING_V1 + ) + ) { + binding!!.callDuration.visibility = View.VISIBLE + val currentTimeInSec = System.currentTimeMillis() / SECOND_IN_MILLIS + elapsedSeconds = currentTimeInSec - callStartTime + + val callTimeTask: Runnable = object : Runnable { + override fun run() { + if (othersInCall) { + binding!!.callDuration.text = DateUtils.formatElapsedTime(elapsedSeconds) + if (elapsedSeconds.toInt() == CALL_TIME_ONE_HOUR) { + showCallRunningSinceOneHourOrMoreInfo() + } + } else { + binding!!.callDuration.text = CALL_DURATION_EMPTY + } + + elapsedSeconds += 1 + callTimeHandler.postDelayed(this, CALL_TIME_COUNTER_DELAY) + } + } + callTimeHandler.post(callTimeTask) + } else { + binding!!.callDuration.visibility = View.GONE + } + } + + private fun showCallRunningSinceOneHourOrMoreInfo() { + binding!!.callDuration.setTypeface(null, Typeface.BOLD) + vibrateShort(context) + Snackbar.make( + binding!!.root, + context.resources.getString(R.string.call_running_since_one_hour), + Snackbar.LENGTH_LONG + ).show() + } + + private fun pullSignalingMessages() { + val signalingApiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.API_V3, 2, 1)) + val delayOnError = AtomicInteger(0) + + ncApi!!.pullSignalingMessages( + credentials, + ApiUtils.getUrlForSignaling( + signalingApiVersion, + baseUrl, + roomToken!! + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .repeatWhen { observable: Observable? -> observable } + .takeWhile { isConnectionEstablished } + .doOnNext { delayOnError.set(0) } + .retryWhen { errors: Observable -> + errors.flatMap { error: Throwable? -> + if (!isConnectionEstablished) { + return@flatMap Observable.error(error) + } + if (delayOnError.get() == 0) { + delayOnError.set(1) + } else if (delayOnError.get() < DELAY_ON_ERROR_STOP_THRESHOLD) { + delayOnError.set(delayOnError.get() * 2) + } + Observable.timer(delayOnError.get().toLong(), TimeUnit.SECONDS) + } + } + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + signalingDisposable = d + } + + override fun onNext(signalingOverall: SignalingOverall) { + receivedSignalingMessages(signalingOverall.ocs!!.signalings) + } + + override fun onError(e: Throwable) { + dispose(signalingDisposable) + } + + override fun onComplete() { + dispose(signalingDisposable) + } + }) + } + + private fun setupAndInitiateWebSocketsConnection() { + if (webSocketConnectionHelper == null) { + webSocketConnectionHelper = WebSocketConnectionHelper() + } + if (webSocketClient == null) { + webSocketClient = WebSocketConnectionHelper.getExternalSignalingInstanceForServer( + externalSignalingServer!!.externalSignalingServer, + conversationUser, + externalSignalingServer!!.externalSignalingTicket, + TextUtils.isEmpty(credentials) + ) + // Although setupAndInitiateWebSocketsConnection could be called several times the web socket is + // initialized just once, so the message receiver is also initialized just once. + signalingMessageReceiver = webSocketClient!!.getSignalingMessageReceiver() + signalingMessageReceiver!!.addListener(localParticipantMessageListener) + signalingMessageReceiver!!.addListener(offerMessageListener) + signalingMessageSender = webSocketClient!!.signalingMessageSender + + // If the connection with the signaling server was not established yet the value will be false, but it will + // be overwritten with the right value once the response to the "hello" message is received. + hasMCU = webSocketClient!!.hasMCU() + Log.d(TAG, "hasMCU is $hasMCU") + + if (hasMCU) { + messageSender = MessageSenderMcu( + signalingMessageSender, + callParticipants.keys, + peerConnectionWrapperList, + webSocketClient!!.sessionId + ) + } else { + messageSender = MessageSenderNoMcu( + signalingMessageSender, + callParticipants.keys, + peerConnectionWrapperList + ) + } + } else { + if (webSocketClient!!.isConnected && currentCallStatus === CallStatus.PUBLISHER_FAILED) { + webSocketClient!!.restartWebSocket() + } + } + joinRoomAndCall() + } + + private fun initiateCall() { + if (isConnectionEstablished) { + Log.d(TAG, "connection already established") + return + } + fetchSignalingSettings() + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) { + if (currentCallStatus === CallStatus.LEAVING) { + return + } + if (webSocketCommunicationEvent.getHashMap() != null) { + when (webSocketCommunicationEvent.getType()) { + "hello" -> { + Log.d(TAG, "onMessageEvent 'hello'") + + hasMCU = webSocketClient!!.hasMCU() + Log.d(TAG, "hasMCU is $hasMCU") + + if (hasMCU) { + messageSender = MessageSenderMcu( + signalingMessageSender, + callParticipants.keys, + peerConnectionWrapperList, + webSocketClient!!.sessionId + ) + } else { + messageSender = MessageSenderNoMcu( + signalingMessageSender, + callParticipants.keys, + peerConnectionWrapperList + ) + } + + if (!webSocketCommunicationEvent.getHashMap()!!.containsKey("oldResumeId")) { + if (currentCallStatus === CallStatus.RECONNECTING) { + hangup(false, false) + } else { + setCallState(CallStatus.RECONNECTING) + runOnUiThread { initiateCall() } + } + } + } + + "roomJoined" -> { + Log.d(TAG, "onMessageEvent 'roomJoined'") + startSendingNick() + if (webSocketCommunicationEvent.getHashMap()!!["roomToken"] == roomToken) { + performCall() + } + } + + "recordingStatus" -> { + Log.d(TAG, "onMessageEvent 'recordingStatus'") + if (webSocketCommunicationEvent.getHashMap()!!.containsKey(KEY_RECORDING_STATE)) { + val recordingStateString = webSocketCommunicationEvent.getHashMap()!![KEY_RECORDING_STATE] + if (recordingStateString != null) { + runOnUiThread { callRecordingViewModel!!.setRecordingState(recordingStateString.toInt()) } + } + } + } + } + } + } + + private fun dispose(disposable: Disposable?) { + if (disposable != null && !disposable.isDisposed) { + disposable.dispose() + } else if (disposable == null) { + if (signalingDisposable != null && !signalingDisposable!!.isDisposed) { + signalingDisposable!!.dispose() + signalingDisposable = null + } + } + } + + private fun receivedSignalingMessages(signalingList: List?) { + if (signalingList != null) { + for (signaling in signalingList) { + try { + receivedSignalingMessage(signaling) + } catch (e: IOException) { + Log.e(TAG, "Failed to process received signaling message", e) + } + } + } + } + + @Throws(IOException::class) + private fun receivedSignalingMessage(signaling: Signaling) { + val messageType = signaling.type + if (!isConnectionEstablished && currentCallStatus !== CallStatus.CONNECTING) { + return + } + + when (messageType) { + "usersInRoom" -> + internalSignalingMessageReceiver.process(signaling.messageWrapper as List?>?) + + "message" -> { + val ncSignalingMessage = LoganSquare.parse( + signaling.messageWrapper.toString(), + NCSignalingMessage::class.java + ) + internalSignalingMessageReceiver.process(ncSignalingMessage) + } + + else -> + Log.e(TAG, "unexpected message type when receiving signaling message") + } + } + + private fun hangup(shutDownView: Boolean, endCallForAll: Boolean) { + Log.d(TAG, "hangup! shutDownView=$shutDownView") + if (shutDownView) { + setCallState(CallStatus.LEAVING) + } + stopCallingSound() + callTimeHandler.removeCallbacksAndMessages(null) + dispose(null) + + if (shutDownView) { + terminateAudioVideo() + } + + val peerConnectionIdsToEnd: MutableList = ArrayList(peerConnectionWrapperList.size) + for (wrapper in peerConnectionWrapperList) { + peerConnectionIdsToEnd.add(wrapper.sessionId) + } + for (sessionId in peerConnectionIdsToEnd) { + endPeerConnection(sessionId, "video") + endPeerConnection(sessionId, "screen") + } + val callParticipantIdsToEnd: MutableList = ArrayList(peerConnectionWrapperList.size) + for (callParticipant in callParticipants.values) { + callParticipantIdsToEnd.add(callParticipant!!.callParticipantModel.sessionId) + } + for (sessionId in callParticipantIdsToEnd) { + removeCallParticipant(sessionId) + } + ApplicationWideCurrentRoomHolder.getInstance().isInCall = false + ApplicationWideCurrentRoomHolder.getInstance().isDialing = false + hangupNetworkCalls(shutDownView, endCallForAll) + } + + private fun terminateAudioVideo() { + if (videoCapturer != null) { + try { + videoCapturer!!.stopCapture() + } catch (e: InterruptedException) { + Log.e(TAG, "Failed to stop capturing while hanging up") + } + videoCapturer!!.dispose() + videoCapturer = null + } + binding!!.selfVideoRenderer.clearImage() + binding!!.selfVideoRenderer.release() + + binding!!.pipSelfVideoRenderer.clearImage() + binding!!.pipSelfVideoRenderer.release() + if (audioSource != null) { + audioSource!!.dispose() + audioSource = null + } + runOnUiThread { + if (audioManager != null) { + audioManager!!.stop() + audioManager = null + } + } + if (videoSource != null) { + videoSource = null + } + if (peerConnectionFactory != null) { + peerConnectionFactory = null + } + localAudioTrack = null + localVideoTrack = null + if (TextUtils.isEmpty(credentials) && hasExternalSignalingServer) { + WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(-1) + } + } + + private fun hangupNetworkCalls(shutDownView: Boolean, endCallForAll: Boolean) { + Log.d(TAG, "hangupNetworkCalls. shutDownView=$shutDownView") + val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1)) + if (localStateBroadcaster != null) { + localStateBroadcaster!!.destroy() + } + if (callParticipantList != null) { + callParticipantList!!.removeObserver(callParticipantListObserver) + callParticipantList!!.destroy() + } + val endCall: Boolean? = if (endCallForAll) true else null + + ncApi!!.leaveCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken!!), endCall) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + if (switchToRoomToken.isNotEmpty()) { + val intent = Intent(context, ChatActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + val bundle = Bundle() + bundle.putBoolean(KEY_SWITCH_TO_ROOM, true) + bundle.putBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, true) + bundle.putString(KEY_ROOM_TOKEN, switchToRoomToken) + bundle.putBoolean(KEY_CALL_VOICE_ONLY, isVoiceOnlyCall) + intent.putExtras(bundle) + startActivity(intent) + finish() + } else if (shutDownView) { + finish() + } else if (currentCallStatus === CallStatus.RECONNECTING || + currentCallStatus === CallStatus.PUBLISHER_FAILED + ) { + initiateCall() + } + } + + override fun onError(e: Throwable) { + Log.w(TAG, "Something went wrong when leaving the call", e) + finish() + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun startVideoCapture(isPortrait: Boolean) { + val (width, height) = if (isPortrait) { + WIDTH_4_TO_3_RATIO to HEIGHT_4_TO_3_RATIO + } else { + WIDTH_16_TO_9_RATIO to HEIGHT_16_TO_9_RATIO + } + + videoCapturer?.let { + it.stopCapture() + it.startCapture(width, height, FRAME_RATE) + } + updateSelfVideoViewPosition(isPortrait) + } + + private fun setupOrientationListener(context: Context) { + var lastAspectRatio = "" + + val orientationEventListener = object : OrientationEventListener(context) { + override fun onOrientationChanged(orientation: Int) { + when (orientation) { + in ANGLE_0..ANGLE_PORTRAIT_RIGHT_THRESHOLD, + in ANGLE_PORTRAIT_LEFT_THRESHOLD..ANGLE_FULL -> { + if (lastAspectRatio != RATIO_4_TO_3) { + lastAspectRatio = RATIO_4_TO_3 + startVideoCapture(true) + } + } + + in ANGLE_LANDSCAPE_RIGHT_THRESHOLD_MIN..ANGLE_LANDSCAPE_RIGHT_THRESHOLD_MAX, + in ANGLE_LANDSCAPE_LEFT_THRESHOLD_MIN..ANGLE_LANDSCAPE_LEFT_THRESHOLD_MAX -> { + if (lastAspectRatio != RATIO_16_TO_9) { + lastAspectRatio = RATIO_16_TO_9 + startVideoCapture(false) + } + } + } + } + } + orientationEventListener.enable() + } + + @Suppress("Detekt.ComplexMethod") + private fun handleCallParticipantsChanged( + joined: Collection, + updated: Collection, + left: Collection, + unchanged: Collection + ) { + Log.d(TAG, "handleCallParticipantsChanged") + + // The signaling session is the same as the Nextcloud session only when the MCU is not used. + var currentSessionId = callSession + if (hasMCU) { + currentSessionId = webSocketClient!!.sessionId + } + Log.d(TAG, " currentSessionId is $currentSessionId") + + val participantsInCall: MutableList = ArrayList() + participantsInCall.addAll(joined) + participantsInCall.addAll(updated) + participantsInCall.addAll(unchanged) + + var isSelfInCall = false + var selfParticipant: Participant? = null + + for (participant in participantsInCall) { + val inCallFlag = participant.inCall + if (participant.sessionId != currentSessionId) { + Log.d( + TAG, + " inCallFlag of participant " + + participant.sessionId!!.substring(0, SESSION_ID_PREFFIX_END) + + " : " + + inCallFlag + ) + } else { + Log.d(TAG, " inCallFlag of currentSessionId: $inCallFlag") + isSelfInCall = inCallFlag != 0L + selfParticipant = participant + } + } + + if (!isSelfInCall && + currentCallStatus !== CallStatus.LEAVING && + ApplicationWideCurrentRoomHolder.getInstance().isInCall + ) { + Log.d(TAG, "Most probably a moderator ended the call for all.") + hangup(shutDownView = true, endCallForAll = false) + return + } + + if (!isSelfInCall) { + Log.d(TAG, "Self not in call, disconnecting from all other sessions") + removeSessions(participantsInCall) + return + } + if (currentCallStatus === CallStatus.LEAVING) { + return + } + if (hasMCU) { + // Ensure that own publishing peer is set up. + getOrCreatePeerConnectionWrapperForSessionIdAndType( + webSocketClient!!.sessionId, + VIDEO_STREAM_TYPE_VIDEO, + true + ) + } + handleJoinedCallParticipantsChanged(selfParticipant, joined, currentSessionId) + + if (othersInCall && currentCallStatus !== CallStatus.IN_CONVERSATION) { + setCallState(CallStatus.IN_CONVERSATION) + } + removeSessions(left) + } + + private fun removeSessions(sessions: Collection) { + for ((_, _, _, _, _, _, _, _, _, _, session) in sessions) { + Log.d(TAG, " session that will be removed is: $session") + endPeerConnection(session, "video") + endPeerConnection(session, "screen") + removeCallParticipant(session) + } + } + + private fun handleJoinedCallParticipantsChanged( + selfParticipant: Participant?, + joined: Collection, + currentSessionId: String? + ) { + var selfJoined = false + val selfParticipantHasAudioOrVideo = participantInCallFlagsHaveAudioOrVideo(selfParticipant) + for (participant in joined) { + val sessionId = participant.sessionId + if (sessionId == null) { + Log.w(TAG, "Null sessionId for call participant, this should not happen: $participant") + continue + } + if (sessionId == currentSessionId) { + selfJoined = true + continue + } + Log.d(TAG, " newSession joined: $sessionId") + addCallParticipant(sessionId) + + if (participant.actorType != null && participant.actorId != null) { + callParticipants[sessionId]!!.setActor(participant.actorType, participant.actorId) + } + + val userId = participant.userId + if (userId != null) { + callParticipants[sessionId]!!.setUserId(userId) + } + + if (participant.internal != null) { + callParticipants[sessionId]!!.setInternal(participant.internal) + } + + val nick: String? = if (hasExternalSignalingServer) { + webSocketClient!!.getDisplayNameForSession(sessionId) + } else { + if (offerAnswerNickProviders[sessionId] != null) offerAnswerNickProviders[sessionId]?.nick else "" + } + + callParticipants[sessionId]!!.setNick(nick) + val participantHasAudioOrVideo = participantInCallFlagsHaveAudioOrVideo(participant) + + // FIXME Without MCU, PeerConnectionWrapper only sends an offer if the local session ID is higher than the + // remote session ID. However, if the other participant does not have audio nor video that participant + // will not send an offer, so no connection is actually established when the remote participant has a + // higher session ID but is not publishing media. + if (hasMCUAndAudioVideo(participantHasAudioOrVideo) || + hasNoMCUAndAudioVideo( + participantHasAudioOrVideo, + selfParticipantHasAudioOrVideo, + sessionId, + currentSessionId!! + ) + ) { + getOrCreatePeerConnectionWrapperForSessionIdAndType(sessionId, VIDEO_STREAM_TYPE_VIDEO, false) + } + } + othersInCall = if (selfJoined) { + joined.size > 1 + } else { + joined.isNotEmpty() + } + } + + private fun hasMCUAndAudioVideo(participantHasAudioOrVideo: Boolean): Boolean = hasMCU && participantHasAudioOrVideo + + private fun hasNoMCUAndAudioVideo( + participantHasAudioOrVideo: Boolean, + selfParticipantHasAudioOrVideo: Boolean, + sessionId: String, + currentSessionId: String + ): Boolean = + !hasMCU && selfParticipantHasAudioOrVideo && (!participantHasAudioOrVideo || sessionId < currentSessionId) + + private fun participantInCallFlagsHaveAudioOrVideo(participant: Participant?): Boolean = + if (participant == null) { + false + } else { + participant.inCall and Participant.InCallFlags.WITH_AUDIO.toLong() > 0 || + !isVoiceOnlyCall && + participant.inCall and Participant.InCallFlags.WITH_VIDEO.toLong() > 0 + } + + private fun getPeerConnectionWrapperForSessionIdAndType(sessionId: String?, type: String): PeerConnectionWrapper? { + for (wrapper in peerConnectionWrapperList) { + if (wrapper.sessionId == sessionId && wrapper.videoStreamType == type) { + return wrapper + } + } + return null + } + + private fun getOrCreatePeerConnectionWrapperForSessionIdAndType( + sessionId: String?, + type: String, + publisher: Boolean + ): PeerConnectionWrapper? { + var peerConnectionWrapper = getPeerConnectionWrapperForSessionIdAndType(sessionId, type) + + return if (peerConnectionWrapper != null) { + peerConnectionWrapper + } else { + if (peerConnectionFactory == null) { + Log.e(TAG, "peerConnectionFactory was null in getOrCreatePeerConnectionWrapperForSessionIdAndType") + Snackbar.make( + binding!!.root, + context.resources.getString(R.string.nc_common_error_sorry), + Snackbar.LENGTH_LONG + ).show() + hangup(shutDownView = true, endCallForAll = false) + return null + } + peerConnectionWrapper = createPeerConnectionWrapperForSessionIdAndType(publisher, sessionId, type) + peerConnectionWrapperList.add(peerConnectionWrapper) + if (!publisher) { + var callParticipant = callParticipants[sessionId] + if (callParticipant == null) { + callParticipant = addCallParticipant(sessionId) + } + if ("screen" == type) { + callParticipant.setScreenPeerConnectionWrapper(peerConnectionWrapper) + } else { + callParticipant.setPeerConnectionWrapper(peerConnectionWrapper) + } + } + if (publisher) { + peerConnectionWrapper.addObserver(selfPeerConnectionObserver) + startSendingNick() + } + peerConnectionWrapper + } + } + + private fun createPeerConnectionWrapperForSessionIdAndType( + publisher: Boolean, + sessionId: String?, + type: String + ): PeerConnectionWrapper { + val tempSdpConstraints: MediaConstraints? + val tempIsMCUPublisher: Boolean + val tempHasMCU: Boolean + val tempLocalStream: MediaStream? + if (hasMCU && publisher) { + tempSdpConstraints = sdpConstraintsForMCUPublisher + tempIsMCUPublisher = true + tempHasMCU = true + tempLocalStream = localStream + } else if (hasMCU) { + tempSdpConstraints = sdpConstraints + tempIsMCUPublisher = false + tempHasMCU = true + tempLocalStream = null + } else { + tempSdpConstraints = sdpConstraints + tempIsMCUPublisher = false + tempHasMCU = false + tempLocalStream = if ("screen" != type) { + localStream + } else { + null + } + } + + return PeerConnectionWrapper( + peerConnectionFactory, + iceServers, + tempSdpConstraints, + sessionId, + callSession, + tempLocalStream, + tempIsMCUPublisher, + tempHasMCU, + type, + signalingMessageReceiver, + signalingMessageSender + ) + } + + private fun addCallParticipant(sessionId: String?): CallParticipant { + val callParticipant = CallParticipant(sessionId, signalingMessageReceiver) + callParticipants[sessionId] = callParticipant + val callParticipantMessageListener: CallParticipantMessageListener = + CallActivityCallParticipantMessageListener(sessionId) + callParticipantMessageListeners[sessionId] = callParticipantMessageListener + signalingMessageReceiver!!.addListener(callParticipantMessageListener, sessionId) + if (!hasExternalSignalingServer) { + val offerAnswerNickProvider = OfferAnswerNickProvider(sessionId) + offerAnswerNickProviders[sessionId] = offerAnswerNickProvider + signalingMessageReceiver!!.addListener( + offerAnswerNickProvider.videoWebRtcMessageListener, + sessionId, + "video" + ) + signalingMessageReceiver!!.addListener( + offerAnswerNickProvider.screenWebRtcMessageListener, + sessionId, + "screen" + ) + } + val callParticipantModel = callParticipant.callParticipantModel + val screenParticipantDisplayItemManager = ScreenParticipantDisplayItemManager(callParticipantModel) + screenParticipantDisplayItemManagers[sessionId] = screenParticipantDisplayItemManager + callParticipantModel.addObserver( + screenParticipantDisplayItemManager, + screenParticipantDisplayItemManagersHandler + ) + val callParticipantEventDisplayer = CallParticipantEventDisplayer(callParticipantModel) + callParticipantEventDisplayers[sessionId] = callParticipantEventDisplayer + callParticipantModel.addObserver(callParticipantEventDisplayer, callParticipantEventDisplayersHandler) + runOnUiThread { addParticipantDisplayItem(callParticipantModel, "video") } + + localStateBroadcaster!!.handleCallParticipantAdded(callParticipant.callParticipantModel) + + return callParticipant + } + + private fun endPeerConnection(sessionId: String?, type: String) { + val peerConnectionWrapper = getPeerConnectionWrapperForSessionIdAndType(sessionId, type) ?: return + if (webSocketClient != null && + webSocketClient!!.sessionId != null && + webSocketClient!!.sessionId == sessionId + ) { + peerConnectionWrapper.removeObserver(selfPeerConnectionObserver) + } + val callParticipant = callParticipants[sessionId] + if (callParticipant != null) { + if ("screen" == type) { + callParticipant.setScreenPeerConnectionWrapper(null) + } else { + callParticipant.setPeerConnectionWrapper(null) + } + } + peerConnectionWrapper.removePeerConnection() + peerConnectionWrapperList.remove(peerConnectionWrapper) + } + + private fun removeCallParticipant(sessionId: String?) { + val callParticipant = callParticipants.remove(sessionId) ?: return + + localStateBroadcaster!!.handleCallParticipantRemoved(callParticipant.callParticipantModel) + + val screenParticipantDisplayItemManager = screenParticipantDisplayItemManagers.remove(sessionId) + callParticipant.callParticipantModel.removeObserver(screenParticipantDisplayItemManager) + val callParticipantEventDisplayer = callParticipantEventDisplayers.remove(sessionId) + callParticipant.callParticipantModel.removeObserver(callParticipantEventDisplayer) + callParticipant.destroy() + val listener = callParticipantMessageListeners.remove(sessionId) + signalingMessageReceiver!!.removeListener(listener) + val offerAnswerNickProvider = offerAnswerNickProviders.remove(sessionId) + if (offerAnswerNickProvider != null) { + signalingMessageReceiver!!.removeListener(offerAnswerNickProvider.videoWebRtcMessageListener) + signalingMessageReceiver!!.removeListener(offerAnswerNickProvider.screenWebRtcMessageListener) + } + runOnUiThread { removeParticipantDisplayItem(sessionId, "video") } + } + + private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) { + val key = "$sessionId-$videoStreamType" + val participant = participantItems.find { it.sessionKey == key } + participant?.destroy() + participantItems.removeAll { it.sessionKey == key } + initGrid() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(configurationChangeEvent: ConfigurationChangeEvent?) { + powerManagerUtils!!.setOrientation(Objects.requireNonNull(resources).configuration.orientation) + initGrid() + } + + private fun updateSelfVideoViewIceConnectionState(iceConnectionState: IceConnectionState) { + val connected = iceConnectionState == IceConnectionState.CONNECTED || + iceConnectionState == IceConnectionState.COMPLETED + + // FIXME In voice only calls there is no video view, so the progress bar would appear floating in the middle of + // nowhere. However, a way to signal that the local participant is not connected to the HPB is still need in + // that case. + if (!connected && !isVoiceOnlyCall) { + binding!!.selfVideoViewProgressBar.visibility = View.VISIBLE + } else { + binding!!.selfVideoViewProgressBar.visibility = View.GONE + } + } + + private fun updateSelfVideoViewPosition(isPortrait: Boolean) { + Log.d(TAG, "updateSelfVideoViewPosition") + if (!isInPipMode) { + val layoutParams = binding!!.selfVideoRenderer.layoutParams as FrameLayout.LayoutParams + if (!isPortrait) { + layoutParams.height = + DisplayUtils.convertDpToPixel(SELFVIDEO_HEIGHT_16_TO_9_RATIO.toFloat(), applicationContext).toInt() + layoutParams.width = + DisplayUtils.convertDpToPixel(SELFVIDEO_WIDTH_16_TO_9_RATIO.toFloat(), applicationContext).toInt() + binding!!.selfVideoViewWrapper.y = SELFVIDEO_POSITION_X_LANDSCAPE + binding!!.selfVideoViewWrapper.x = SELFVIDEO_POSITION_Y_LANDSCAPE + } else { + layoutParams.height = + DisplayUtils.convertDpToPixel(SELFVIDEO_HEIGHT_4_TO_3_RATIO.toFloat(), applicationContext).toInt() + layoutParams.width = + DisplayUtils.convertDpToPixel(SELFVIDEO_WIDTH_4_TO_3_RATIO.toFloat(), applicationContext).toInt() + binding!!.selfVideoViewWrapper.y = SELFVIDEO_POSITION_X_PORTRAIT + binding!!.selfVideoViewWrapper.x = SELFVIDEO_POSITION_Y_PORTRAIT + } + binding!!.selfVideoRenderer.layoutParams = layoutParams + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(proximitySensorEvent: ProximitySensorEvent) { + if (!isVoiceOnlyCall) { + val enableVideo = proximitySensorEvent.proximitySensorEventType == + ProximitySensorEvent.ProximitySensorEventType.SENSOR_FAR && + videoOn + if (permissionUtil!!.isCameraPermissionGranted() && + isConnectingOrEstablished() && + videoOn && + enableVideo != localVideoTrack!!.enabled() + ) { + toggleMedia(enableVideo, true) + } + } + } + + private fun isConnectingOrEstablished(): Boolean = + currentCallStatus === CallStatus.CONNECTING || isConnectionEstablished + + private fun startSendingNick() { + val dataChannelMessage = DataChannelMessage() + dataChannelMessage.type = "nickChanged" + val nickChangedPayload: MutableMap = HashMap() + nickChangedPayload["userid"] = conversationUser!!.userId!! + nickChangedPayload["name"] = conversationUser!!.displayName!! + dataChannelMessage.payloadMap = nickChangedPayload.toMap() + for (peerConnectionWrapper in peerConnectionWrapperList) { + if (peerConnectionWrapper.isMCUPublisher) { + Observable + .interval(1, TimeUnit.SECONDS) + .repeatUntil { !isConnectionEstablished || isDestroyed } + .observeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(aLong: Long) { + peerConnectionWrapper.send(dataChannelMessage) + } + + override fun onError(e: Throwable) { + // unused atm + } + + override fun onComplete() { + // unused atm + } + }) + break + } + } + } + + private fun addParticipantDisplayItem(callParticipantModel: CallParticipantModel, videoStreamType: String) { + if (callParticipantModel.isInternal == true) return + + val defaultGuestNick = resources.getString(R.string.nc_nick_guest) + val participantDisplayItem = ParticipantDisplayItem( + context = context, + baseUrl = baseUrl!!, + defaultGuestNick = defaultGuestNick, + rootEglBase = rootEglBase!!, + streamType = videoStreamType, + roomToken = roomToken!!, + callParticipantModel = callParticipantModel + ) + + val sessionKey = participantDisplayItem.sessionKey + + if (participantItems.none { it.sessionKey == sessionKey }) { + participantItems.add(participantDisplayItem) + } + + initGrid() + } + + private fun setCallState(callState: CallStatus) { + if (currentCallStatus == null || currentCallStatus !== callState) { + currentCallStatus = callState + if (handler == null) { + handler = Handler(Looper.getMainLooper()) + } else { + handler!!.removeCallbacksAndMessages(null) + } + when (callState) { + CallStatus.CONNECTING -> handler!!.post { handleCallStateConnected() } + CallStatus.CALLING_TIMEOUT -> handler!!.post { handleCallStateCallingTimeout() } + CallStatus.PUBLISHER_FAILED -> handler!!.post { handleCallStatePublisherFailed() } + CallStatus.RECONNECTING -> handler!!.post { handleCallStateReconnecting() } + CallStatus.JOINED -> { + handler!!.postDelayed({ setCallState(CallStatus.CALLING_TIMEOUT) }, CALLING_TIMEOUT) + handler!!.post { handleCallStateJoined() } + } + + CallStatus.IN_CONVERSATION -> handler!!.post { handleCallStateInConversation() } + CallStatus.OFFLINE -> handler!!.post { handleCallStateOffline() } + CallStatus.LEAVING -> handler!!.post { handleCallStateLeaving() } + } + } + } + + private fun handleCallStateLeaving() { + if (!isDestroyed) { + stopCallingSound() + binding!!.callModeTextView.text = descriptionForCallType + binding!!.callStates.callStateTextView.setText(R.string.nc_leaving_call) + binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE + binding!!.composeParticipantGrid.visibility = View.INVISIBLE + binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE + binding!!.callStates.errorImageView.visibility = View.GONE + } + } + + private fun handleCallStateOffline() { + stopCallingSound() + binding!!.callStates.callStateTextView.setText(R.string.nc_offline) + if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) { + binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE + } + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE + } + if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) { + binding!!.callStates.callStateProgressBar.visibility = View.GONE + } + binding!!.callStates.errorImageView.setImageResource(R.drawable.ic_signal_wifi_off_white_24dp) + if (binding!!.callStates.errorImageView.visibility != View.VISIBLE) { + binding!!.callStates.errorImageView.visibility = View.VISIBLE + } + } + + private fun handleCallStateInConversation() { + stopCallingSound() + binding!!.callModeTextView.text = descriptionForCallType + if (!isVoiceOnlyCall) { + binding!!.callInfosLinearLayout.visibility = View.GONE + } + if (!isPushToTalkActive) { + animateCallControls(false, FIVE_SECONDS) + } + if (binding!!.callStates.callStateRelativeLayout.visibility != View.INVISIBLE) { + binding!!.callStates.callStateRelativeLayout.visibility = View.INVISIBLE + } + if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) { + binding!!.callStates.callStateProgressBar.visibility = View.GONE + } + if (binding!!.composeParticipantGrid.visibility != View.VISIBLE) { + binding!!.composeParticipantGrid.visibility = View.VISIBLE + } + if (binding!!.callStates.errorImageView.visibility != View.GONE) { + binding!!.callStates.errorImageView.visibility = View.GONE + } + } + + private fun handleCallStateJoined() { + binding!!.callModeTextView.text = descriptionForCallType + if (isIncomingCallFromNotification) { + binding!!.callStates.callStateTextView.setText(R.string.nc_call_incoming) + } else { + binding!!.callStates.callStateTextView.setText(R.string.nc_call_ringing) + } + if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) { + binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE + } + if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) { + binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE + } + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE + } + if (binding!!.callStates.errorImageView.visibility != View.GONE) { + binding!!.callStates.errorImageView.visibility = View.GONE + } + } + + private fun handleCallStateReconnecting() { + playCallingSound() + binding!!.callStates.callStateTextView.setText(R.string.nc_call_reconnecting) + binding!!.callModeTextView.text = descriptionForCallType + if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) { + binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE + } + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE + } + if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) { + binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE + } + if (binding!!.callStates.errorImageView.visibility != View.GONE) { + binding!!.callStates.errorImageView.visibility = View.GONE + } + } + + private fun handleCallStatePublisherFailed() { + // No calling sound when the publisher failed + binding!!.callStates.callStateTextView.setText(R.string.nc_call_reconnecting) + binding!!.callModeTextView.text = descriptionForCallType + if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) { + binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE + } + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE + } + if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) { + binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE + } + if (binding!!.callStates.errorImageView.visibility != View.GONE) { + binding!!.callStates.errorImageView.visibility = View.GONE + } + } + + private fun handleCallStateCallingTimeout() { + hangup(shutDownView = false, endCallForAll = false) + binding!!.callStates.callStateTextView.setText(R.string.nc_call_timeout) + binding!!.callModeTextView.text = descriptionForCallType + if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) { + binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE + } + if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) { + binding!!.callStates.callStateProgressBar.visibility = View.GONE + } + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE + } + binding!!.callStates.errorImageView.setImageResource(R.drawable.ic_av_timer_timer_24dp) + if (binding!!.callStates.errorImageView.visibility != View.VISIBLE) { + binding!!.callStates.errorImageView.visibility = View.VISIBLE + } + } + + private fun handleCallStateConnected() { + playCallingSound() + if (isIncomingCallFromNotification) { + binding!!.callStates.callStateTextView.setText(R.string.nc_call_incoming) + } else { + binding!!.callStates.callStateTextView.setText(R.string.nc_call_ringing) + } + binding!!.callConversationNameTextView.text = conversationName + binding!!.callModeTextView.text = descriptionForCallType + if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) { + binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE + } + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE + } + if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) { + binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE + } + if (binding!!.callStates.errorImageView.visibility != View.GONE) { + binding!!.callStates.errorImageView.visibility = View.GONE + } + } + + private val descriptionForCallType: String + get() { + val appName = resources.getString(R.string.nc_app_product_name) + return if (isVoiceOnlyCall) { + String.format(resources.getString(R.string.nc_call_voice), appName) + } else { + String.format(resources.getString(R.string.nc_call_video), appName) + } + } + + private fun playCallingSound() { + stopCallingSound() + val ringtoneUri: Uri? = if (isIncomingCallFromNotification) { + getCallRingtoneUri(applicationContext, appPreferences) + } else { + ("android.resource://" + applicationContext.packageName + "/raw/tr110_1_kap8_3_freiton1").toUri() + } + if (ringtoneUri != null) { + mediaPlayer = MediaPlayer() + try { + mediaPlayer!!.setDataSource(this, ringtoneUri) + mediaPlayer!!.isLooping = true + val audioAttributes = AudioAttributes.Builder().setContentType( + AudioAttributes.CONTENT_TYPE_SONIFICATION + ) + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .build() + mediaPlayer!!.setAudioAttributes(audioAttributes) + mediaPlayer!!.setOnPreparedListener { mp: MediaPlayer? -> mediaPlayer!!.start() } + mediaPlayer!!.prepareAsync() + } catch (e: IOException) { + Log.e(TAG, "Failed to play sound") + } + } + } + + private fun stopCallingSound() { + if (mediaPlayer != null) { + try { + if (mediaPlayer!!.isPlaying) { + mediaPlayer!!.stop() + } + } catch (e: IllegalStateException) { + Log.e(TAG, "mediaPlayer was not initialized", e) + } finally { + if (mediaPlayer != null) { + mediaPlayer!!.release() + } + mediaPlayer = null + } + } + } + + fun addReactionForAnimation(emoji: String?, displayName: String?) { + reactionAnimator!!.addReaction(emoji!!, displayName!!) + } + + /** + * Temporary implementation of SignalingMessageReceiver until signaling related code is extracted from + * CallActivity. + * + * + * All listeners are called in the main thread. + */ + private class InternalSignalingMessageReceiver : SignalingMessageReceiver() { + fun process(users: List?>?) { + processUsersInRoom(users) + } + + fun process(message: NCSignalingMessage?) { + processSignalingMessage(message) + } + } + + private inner class OfferAnswerNickProvider(private val sessionId: String?) { + val videoWebRtcMessageListener: WebRtcMessageListener = WebRtcMessageListener() + val screenWebRtcMessageListener: WebRtcMessageListener = WebRtcMessageListener() + var nick: String? = null + private set + + private inner class WebRtcMessageListener : SignalingMessageReceiver.WebRtcMessageListener { + override fun onOffer(sdp: String, nick: String?) { + onOfferOrAnswer(nick) + } + + override fun onAnswer(sdp: String, nick: String?) { + onOfferOrAnswer(nick) + } + + override fun onCandidate(sdpMid: String, sdpMLineIndex: Int, sdp: String) { + // unused atm + } + + override fun onEndOfCandidates() { + // unused atm + } + } + + private fun onOfferOrAnswer(nick: String?) { + this.nick = nick + if (callParticipants[sessionId] != null) { + callParticipants[sessionId]!!.setNick(nick) + } + } + } + + private inner class CallActivityCallParticipantMessageListener(private val sessionId: String?) : + CallParticipantMessageListener { + override fun onRaiseHand(state: Boolean, timestamp: Long) { + // unused atm + } + + override fun onReaction(reaction: String) { + // unused atm + } + + override fun onUnshareScreen() { + endPeerConnection(sessionId, "screen") + } + } + + private inner class CallActivitySelfPeerConnectionObserver : PeerConnectionObserver { + override fun onStreamAdded(mediaStream: MediaStream) { + // unused atm + } + + override fun onStreamRemoved(mediaStream: MediaStream) { + // unused atm + } + + override fun onIceConnectionStateChanged(iceConnectionState: IceConnectionState) { + runOnUiThread { + updateSelfVideoViewIceConnectionState(iceConnectionState) + if (iceConnectionState == IceConnectionState.FAILED) { + setCallState(CallStatus.PUBLISHER_FAILED) + webSocketClient!!.clearResumeId() + hangup(false, false) + } + } + } + } + + private inner class ScreenParticipantDisplayItemManager(private val callParticipantModel: CallParticipantModel) : + CallParticipantModel.Observer { + override fun onChange() { + val sessionId = callParticipantModel.sessionId + if (callParticipantModel.screenIceConnectionState == null) { + removeParticipantDisplayItem(sessionId, "screen") + return + } + val screenParticipantDisplayItem = participantItems.find { it.sessionKey == "$sessionId-screen" } + if (screenParticipantDisplayItem == null) { + addParticipantDisplayItem(callParticipantModel, "screen") + } + } + + override fun onReaction(reaction: String) { + // unused atm + } + } + + private inner class CallParticipantEventDisplayer(private val callParticipantModel: CallParticipantModel) : + CallParticipantModel.Observer { + private var raisedHand: Boolean + + init { + raisedHand = if (callParticipantModel.raisedHand != null) callParticipantModel.raisedHand.state else false + } + + @SuppressLint("StringFormatInvalid") + override fun onChange() { + if (callParticipantModel.raisedHand == null || !callParticipantModel.raisedHand.state) { + raisedHand = false + return + } + if (raisedHand) { + return + } + raisedHand = true + val nick = callParticipantModel.nick + Snackbar.make( + binding!!.root, + String.format(context.resources.getString(R.string.nc_call_raised_hand), nick), + Snackbar.LENGTH_LONG + ).show() + } + + override fun onReaction(reaction: String) { + addReactionForAnimation(reaction, callParticipantModel.nick) + } + } + + private inner class InternalSignalingMessageSender : SignalingMessageSender { + override fun send(ncSignalingMessage: NCSignalingMessage) { + addLocalParticipantNickIfNeeded(ncSignalingMessage) + val serializedNcSignalingMessage: String = try { + LoganSquare.serialize(ncSignalingMessage) + } catch (e: IOException) { + Log.e(TAG, "Failed to serialize signaling message", e) + return + } + + // The message wrapper can not be defined in a JSON model to be directly serialized, as sent messages + // need to be serialized twice; first the signaling message, and then the wrapper as a whole. Received + // messages, on the other hand, just need to be deserialized once. + val stringBuilder = StringBuilder() + stringBuilder.append('{') + .append("\"fn\":\"") + .append(StringEscapeUtils.escapeJson(serializedNcSignalingMessage)) + .append('\"') + .append(',') + .append("\"sessionId\":") + .append('\"').append(StringEscapeUtils.escapeJson(callSession)).append('\"') + .append(',') + .append("\"ev\":\"message\"") + .append('}') + val strings: MutableList = ArrayList() + val stringToSend = stringBuilder.toString() + strings.add(stringToSend) + val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.API_V3, 2, 1)) + ncApi!!.sendSignalingMessages( + credentials, + ApiUtils.getUrlForSignaling(apiVersion, baseUrl, roomToken!!), + strings.toString() + ) + .retry(API_RETRIES) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(signalingOverall: SignalingOverall) { + // When sending messages to the internal signaling server the response has been empty since + // Talk v2.9.0, so it is not really needed to process it, but there is no harm either in + // doing that, as technically messages could be returned. + receivedSignalingMessages(signalingOverall.ocs!!.signalings) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to send signaling message", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + /** + * Adds the local participant nick to offers and answers. + * + * + * For legacy reasons the offers and answers sent when the internal signaling server is used are expected to + * provide the nick of the local participant. + * + * @param ncSignalingMessage the message to add the nick to + */ + private fun addLocalParticipantNickIfNeeded(ncSignalingMessage: NCSignalingMessage) { + val type = ncSignalingMessage.type + if ("offer" != type && "answer" != type) { + return + } + val payload = ncSignalingMessage.payload + ?: // Broken message, this should not happen + return + payload.nick = conversationUser!!.displayName + } + } + + private inner class MicrophoneButtonTouchListener : OnTouchListener { + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View, event: MotionEvent): Boolean { + v.onTouchEvent(event) + if (event.action == MotionEvent.ACTION_UP && isPushToTalkActive) { + isPushToTalkActive = false + binding!!.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px) + pulseAnimation!!.stop() + toggleMedia(false, false) + animateCallControls(false, FIVE_SECONDS) + } + return true + } + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(networkEvent: NetworkEvent) { + if (networkEvent.networkConnectionEvent == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) { + if (handler != null) { + handler!!.removeCallbacksAndMessages(null) + } + } else if (networkEvent.networkConnectionEvent == + NetworkEvent.NetworkConnectionEvent.NETWORK_DISCONNECTED + ) { + if (handler != null) { + handler!!.removeCallbacksAndMessages(null) + } + } + } + + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + Log.d(TAG, "onPictureInPictureModeChanged") + Log.d(TAG, "isInPictureInPictureMode= $isInPictureInPictureMode") + isInPipMode = isInPictureInPictureMode + if (isInPictureInPictureMode) { + mReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (MICROPHONE_PIP_INTENT_NAME != intent.action) { + return + } + when (intent.getIntExtra(MICROPHONE_PIP_INTENT_EXTRA_ACTION, 0)) { + MICROPHONE_PIP_REQUEST_MUTE, MICROPHONE_PIP_REQUEST_UNMUTE -> onMicrophoneClick() + } + } + } + registerPermissionHandlerBroadcastReceiver( + mReceiver, + IntentFilter(MICROPHONE_PIP_INTENT_NAME), + permissionUtil!!.privateBroadcastPermission, + null, + ReceiverFlag.NotExported + ) + updateUiForPipMode() + } else { + unregisterReceiver(mReceiver) + mReceiver = null + updateUiForNormalMode() + } + } + + private fun updatePictureInPictureActions(@DrawableRes iconId: Int, title: String?, requestCode: Int) { + if (isPipModePossible) { + val actions = ArrayList() + val icon = Icon.createWithResource(this, iconId) + val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_IMMUTABLE + } else { + 0 + } + val intent = PendingIntent.getBroadcast( + this, + requestCode, + Intent(MICROPHONE_PIP_INTENT_NAME).putExtra(MICROPHONE_PIP_INTENT_EXTRA_ACTION, requestCode), + intentFlag + ) + actions.add(RemoteAction(icon, title!!, title, intent)) + mPictureInPictureParamsBuilder.setActions(actions) + setPictureInPictureParams(mPictureInPictureParamsBuilder.build()) + } + } + + override fun updateUiForPipMode() { + Log.d(TAG, "updateUiForPipMode") + binding!!.callControls.visibility = View.GONE + binding!!.callInfosLinearLayout.visibility = View.GONE + binding!!.selfVideoViewWrapper.visibility = View.GONE + binding!!.callStates.callStateRelativeLayout.visibility = View.GONE + binding!!.pipCallConversationNameTextView.text = conversationName + + binding!!.selfVideoRenderer.clearImage() + binding!!.selfVideoRenderer.release() + + if (participantItems.size == 1) { + binding!!.pipOverlay.visibility = View.GONE + } else { + binding!!.composeParticipantGrid.visibility = View.GONE + + if (localVideoTrack?.enabled() == true) { + binding!!.pipOverlay.visibility = View.VISIBLE + binding!!.pipSelfVideoRenderer.visibility = View.VISIBLE + + try { + binding!!.pipSelfVideoRenderer.init(rootEglBase!!.eglBaseContext, null) + } catch (e: IllegalStateException) { + Log.d(TAG, "pipGroupVideoRenderer already initialized", e) + } + binding!!.pipSelfVideoRenderer.setZOrderMediaOverlay(true) + // disabled because it causes some devices to crash + binding!!.pipSelfVideoRenderer.setEnableHardwareScaler(false) + binding!!.pipSelfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) + + localVideoTrack?.addSink(binding?.pipSelfVideoRenderer) + } else { + binding!!.pipOverlay.visibility = View.VISIBLE + binding!!.pipSelfVideoRenderer.visibility = View.GONE + } + } + } + + override fun updateUiForNormalMode() { + Log.d(TAG, "updateUiForNormalMode") + binding!!.pipOverlay.visibility = View.GONE + binding!!.composeParticipantGrid.visibility = View.VISIBLE + + if (isVoiceOnlyCall) { + binding!!.callControls.visibility = View.VISIBLE + } else { + // animateCallControls needs this to be invisible for a check. + binding!!.callControls.visibility = View.INVISIBLE + } + initViews() + binding!!.callInfosLinearLayout.visibility = View.VISIBLE + binding!!.selfVideoViewWrapper.visibility = View.VISIBLE + } + + override fun suppressFitsSystemWindows() { + binding!!.callLayout.fitsSystemWindows = false + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + eventBus.post(ConfigurationChangeEvent()) + } + + val isAllowedToStartOrStopRecording: Boolean + get() = ( + isCallRecordingAvailable(conversationUser!!.capabilities!!.spreedCapability!!) && + isModerator + ) + val isAllowedToRaiseHand: Boolean + get() = hasSpreedFeatureCapability( + conversationUser.capabilities!!.spreedCapability!!, + SpreedFeatures.RAISE_HAND + ) || + isBreakoutRoom + + private inner class SelfVideoTouchListener : OnTouchListener { + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(view: View, event: MotionEvent): Boolean { + val duration = event.eventTime - event.downTime + if (event.actionMasked == MotionEvent.ACTION_MOVE) { + val newY = event.rawY - binding!!.selfVideoViewWrapper.height / 2f + val newX = event.rawX - binding!!.selfVideoViewWrapper.width / 2f + binding!!.selfVideoViewWrapper.y = newY + binding!!.selfVideoViewWrapper.x = newX + } else if (event.actionMasked == MotionEvent.ACTION_UP && duration < SWITCH_CAMERA_THRESHOLD_DURATION) { + switchCamera() + } + return true + } + } + + companion object { + var active = false + + // const val VIDEO_STREAM_TYPE_SCREEN = "screen" + const val VIDEO_STREAM_TYPE_VIDEO = "video" + const val TAG = "CallActivity" + private val PERMISSIONS_CAMERA = arrayOf( + Manifest.permission.CAMERA + ) + private val PERMISSIONS_MICROPHONE = arrayOf( + Manifest.permission.RECORD_AUDIO + ) + private const val MICROPHONE_PIP_INTENT_NAME = "microphone_pip_intent" + private const val MICROPHONE_PIP_INTENT_EXTRA_ACTION = "microphone_pip_action" + private const val MICROPHONE_PIP_REQUEST_MUTE = 1 + private const val MICROPHONE_PIP_REQUEST_UNMUTE = 2 + + const val OPACITY_ENABLED = 1.0f + const val OPACITY_DISABLED = 0.7f + const val OPACITY_INVISIBLE = 0.0f + + const val SECOND_IN_MILLIS: Long = 1000 + const val CALL_TIME_COUNTER_DELAY: Long = 1000 + const val CALL_TIME_ONE_HOUR = 3600 + const val CALL_DURATION_EMPTY = "--:--" + const val API_RETRIES: Long = 3 + + const val SWITCH_CAMERA_THRESHOLD_DURATION = 100 + + private const val SAMPLE_RATE = 8000 + private const val MICROPHONE_VALUE_THRESHOLD = 20 + private const val MICROPHONE_VALUE_SLEEP: Long = 1000 + + private const val FRAME_RATE: Int = 30 + private const val WIDTH_16_TO_9_RATIO: Int = 1280 + private const val HEIGHT_16_TO_9_RATIO: Int = 720 + private const val WIDTH_4_TO_3_RATIO: Int = 640 + private const val HEIGHT_4_TO_3_RATIO: Int = 480 + + private const val RATIO_4_TO_3 = "RATIO_4_TO_3" + private const val RATIO_16_TO_9 = "RATIO_16_TO_9" + private const val ANGLE_0 = 0 + private const val ANGLE_FULL = 360 + private const val ANGLE_PORTRAIT_RIGHT_THRESHOLD = 30 + private const val ANGLE_PORTRAIT_LEFT_THRESHOLD = 330 + private const val ANGLE_LANDSCAPE_RIGHT_THRESHOLD_MIN = 80 + private const val ANGLE_LANDSCAPE_RIGHT_THRESHOLD_MAX = 100 + private const val ANGLE_LANDSCAPE_LEFT_THRESHOLD_MIN = 260 + private const val ANGLE_LANDSCAPE_LEFT_THRESHOLD_MAX = 280 + + private const val SELFVIDEO_WIDTH_4_TO_3_RATIO = 80 + private const val SELFVIDEO_HEIGHT_4_TO_3_RATIO = 104 + private const val SELFVIDEO_WIDTH_16_TO_9_RATIO = 136 + private const val SELFVIDEO_HEIGHT_16_TO_9_RATIO = 80 + + private const val SELFVIDEO_POSITION_X_LANDSCAPE = 50F + private const val SELFVIDEO_POSITION_Y_LANDSCAPE = 50F + private const val SELFVIDEO_POSITION_X_PORTRAIT = 300F + private const val SELFVIDEO_POSITION_Y_PORTRAIT = 50F + + private const val FIVE_SECONDS: Long = 5000 + private const val CALLING_TIMEOUT: Long = 45000 + private const val INTRO_ANIMATION_DURATION: Long = 300 + private const val FADE_IN_ANIMATION_DURATION: Long = 400 + private const val PULSE_ANIMATION_DURATION: Int = 310 + private const val CALL_CONTROLLS_ANIMATION_DELAY: Long = 7500 + + private const val SPOTLIGHT_HEADING_SIZE: Int = 20 + private const val SPOTLIGHT_SUBHEADING_SIZE: Int = 16 + + private const val DELAY_ON_ERROR_STOP_THRESHOLD: Int = 16 + + private const val SESSION_ID_PREFFIX_END: Int = 4 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallBaseActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallBaseActivity.java new file mode 100644 index 0000000..64cfcef --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/CallBaseActivity.java @@ -0,0 +1,149 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.activities; + +import android.annotation.SuppressLint; +import android.app.AppOpsManager; +import android.app.KeyguardManager; +import android.app.PictureInPictureParams; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.util.Rational; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import com.nextcloud.talk.BuildConfig; + +import androidx.activity.OnBackPressedCallback; + +public abstract class CallBaseActivity extends BaseActivity { + + public static final String TAG = "CallBaseActivity"; + + public PictureInPictureParams.Builder mPictureInPictureParamsBuilder; + public Boolean isInPipMode = Boolean.FALSE; + long onCreateTime; + + + private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (isPipModePossible()) { + enterPipMode(); + } + } + }; + + @SuppressLint("ClickableViewAccessibility") + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + onCreateTime = System.currentTimeMillis(); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + dismissKeyguard(); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + if (isPipModePossible()) { + mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder(); + } + + getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); + } + + public void hideNavigationIfNoPipAvailable(){ + if (!isPipModePossible()) { + getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + suppressFitsSystemWindows(); + } + } + + void dismissKeyguard() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true); + setTurnScreenOn(true); + KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); + keyguardManager.requestDismissKeyguard(this, null); + } else { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + } + } + + void enableKeyguard() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(false); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + } + } + + @Override + public void onStop() { + super.onStop(); + if (isInPipMode) { + finish(); + } + } + + @Override + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + long onUserLeaveHintTime = System.currentTimeMillis(); + long diff = onUserLeaveHintTime - onCreateTime; + Log.d(TAG, "onUserLeaveHintTime - onCreateTime: " + diff); + + if (diff < 3000) { + Log.d(TAG, "enterPipMode skipped"); + } else { + enterPipMode(); + } + } + + void enterPipMode() { + enableKeyguard(); + if (isPipModePossible()) { + Rational pipRatio = new Rational(300, 500); + mPictureInPictureParamsBuilder.setAspectRatio(pipRatio); + enterPictureInPictureMode(mPictureInPictureParamsBuilder.build()); + } else { + // we don't support other solutions than PIP to have a call in the background. + // If PIP is not available the call is ended when user presses the home button. + Log.d(TAG, "Activity was finished because PIP is not available."); + finish(); + } + } + + boolean isPipModePossible() { + boolean deviceHasPipFeature = getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); + + AppOpsManager appOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); + boolean isPipFeatureGranted = appOpsManager.checkOpNoThrow( + AppOpsManager.OPSTR_PICTURE_IN_PICTURE, + android.os.Process.myUid(), + BuildConfig.APPLICATION_ID) == AppOpsManager.MODE_ALLOWED; + return deviceHasPipFeature && isPipFeatureGranted; + } + + public abstract void updateUiForPipMode(); + + public abstract void updateUiForNormalMode(); + + public abstract void suppressFitsSystemWindows(); +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallStatus.kt b/app/src/main/java/com/nextcloud/talk/activities/CallStatus.kt new file mode 100644 index 0000000..dd29a1f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/CallStatus.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.activities + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +enum class CallStatus : Parcelable { + CONNECTING, + CALLING_TIMEOUT, + JOINED, + IN_CONVERSATION, + RECONNECTING, + OFFLINE, + LEAVING, + PUBLISHER_FAILED +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt new file mode 100644 index 0000000..4d5d3f7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -0,0 +1,289 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.activities + +import android.app.KeyguardManager +import android.content.Intent +import android.os.Bundle +import android.provider.ContactsContract +import android.text.TextUtils +import android.util.Log +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import autodagger.AutoInjector +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.account.ServerSelectionActivity +import com.nextcloud.talk.account.WebViewLoginActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.conversationlist.ConversationsListActivity +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityMainBinding +import com.nextcloud.talk.invitation.InvitationsActivity +import com.nextcloud.talk.lock.LockedActivity +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ClosedInterfaceImpl +import com.nextcloud.talk.utils.SecurityUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import io.reactivex.Observer +import io.reactivex.SingleObserver +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class MainActivity : + BaseActivity(), + ActionBarProvider { + + lateinit var binding: ActivityMainBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finish() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + Log.d(TAG, "onCreate: Activity: " + System.identityHashCode(this).toString()) + + super.onCreate(savedInstanceState) + + ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + lockScreenIfConditionsApply() + } + }) + + // Set the default theme to replace the launch screen theme. + setTheme(R.style.AppTheme) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + setSupportActionBar(binding.toolbar) + + handleIntent(intent) + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + fun lockScreenIfConditionsApply() { + val keyguardManager = getSystemService(KEYGUARD_SERVICE) as KeyguardManager + if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) { + if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)) { + val lockIntent = Intent(context, LockedActivity::class.java) + startActivity(lockIntent) + } + } + } + + private fun launchServerSelection() { + if (isBrandingUrlSet()) { + val intent = Intent(context, WebViewLoginActivity::class.java) + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_BASE_URL, resources.getString(R.string.weblogin_url)) + intent.putExtras(bundle) + startActivity(intent) + } else { + val intent = Intent(context, ServerSelectionActivity::class.java) + startActivity(intent) + } + } + + private fun isBrandingUrlSet() = !TextUtils.isEmpty(resources.getString(R.string.weblogin_url)) + + override fun onStart() { + Log.d(TAG, "onStart: Activity: " + System.identityHashCode(this).toString()) + super.onStart() + } + + override fun onResume() { + Log.d(TAG, "onResume: Activity: " + System.identityHashCode(this).toString()) + super.onResume() + + if (appPreferences.isScreenLocked) { + SecurityUtils.createKey(appPreferences.screenLockTimeout) + } + } + + override fun onPause() { + Log.d(TAG, "onPause: Activity: " + System.identityHashCode(this).toString()) + super.onPause() + } + + override fun onStop() { + Log.d(TAG, "onStop: Activity: " + System.identityHashCode(this).toString()) + super.onStop() + } + + private fun openConversationList() { + val intent = Intent(this, ConversationsListActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.putExtras(Bundle()) + startActivity(intent) + } + + private fun handleActionFromContact(intent: Intent) { + if (intent.action == Intent.ACTION_VIEW && intent.data != null) { + val cursor = contentResolver.query(intent.data!!, null, null, null, null) + + var userId = "" + if (cursor != null) { + if (cursor.moveToFirst()) { + // userId @ server + userId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.DATA1)) + } + + cursor.close() + } + + when (intent.type) { + "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" -> { + val user = userId.substringBeforeLast("@") + val baseUrl = userId.substringAfterLast("@") + + if (currentUserProvider.currentUser.blockingGet()?.baseUrl!!.endsWith(baseUrl) == true) { + startConversation(user) + } else { + Snackbar.make( + binding.root, + R.string.nc_phone_book_integration_account_not_found, + Snackbar.LENGTH_LONG + ).show() + } + } + } + } + } + + private fun startConversation(userId: String) { + val roomType = "1" + + val currentUser = currentUserProvider.currentUser.blockingGet() + + val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, 1)) + val credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token) + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + version = apiVersion, + baseUrl = currentUser?.baseUrl!!, + roomType = roomType, + invite = userId + ) + + ncApi.createRoom( + credentials, + retrofitBucket.url, + retrofitBucket.queryMap + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(roomOverall: RoomOverall) { + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token) + + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + startActivity(chatIntent) + } + + override fun onError(e: Throwable) { + // unused atm + } + + override fun onComplete() { + // unused atm + } + }) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + Log.d(TAG, "onNewIntent Activity: " + System.identityHashCode(this).toString()) + handleIntent(intent) + } + + private fun handleIntent(intent: Intent) { + handleActionFromContact(intent) + + val internalUserId = intent.extras?.getLong(BundleKeys.KEY_INTERNAL_USER_ID) + + var user: User? = null + if (internalUserId != null) { + user = userManager.getUserWithId(internalUserId).blockingGet() + } + + if (user != null && userManager.setUserAsActive(user).blockingGet()) { + if (intent.hasExtra(BundleKeys.KEY_REMOTE_TALK_SHARE)) { + if (intent.getBooleanExtra(BundleKeys.KEY_REMOTE_TALK_SHARE, false)) { + val invitationsIntent = Intent(this, InvitationsActivity::class.java) + startActivity(invitationsIntent) + } + } else { + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(intent.extras!!) + startActivity(chatIntent) + } + } else { + userManager.users.subscribe(object : SingleObserver> { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onSuccess(users: List) { + if (users.isNotEmpty()) { + ClosedInterfaceImpl().setUpPushTokenRegistration() + runOnUiThread { + openConversationList() + } + } else { + runOnUiThread { + launchServerSelection() + } + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error loading existing users", e) + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_SHORT + ).show() + } + }) + } + } + + companion object { + private val TAG = MainActivity::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/TakePhotoActivity.java b/app/src/main/java/com/nextcloud/talk/activities/TakePhotoActivity.java new file mode 100644 index 0000000..a66f60e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/TakePhotoActivity.java @@ -0,0 +1,423 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Stefan Niedermann + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.activities; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.net.Uri; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.Size; +import android.view.OrientationEventListener; +import android.view.ScaleGestureDetector; +import android.view.Surface; +import android.view.View; +import com.google.android.material.snackbar.Snackbar; +import com.google.common.util.concurrent.ListenableFuture; +import com.nextcloud.talk.R; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.databinding.ActivityTakePictureBinding; +import com.nextcloud.talk.models.TakePictureViewModel; +import com.nextcloud.talk.ui.theme.ViewThemeUtils; +import com.nextcloud.talk.utils.BitmapShrinker; +import com.nextcloud.talk.utils.FileUtils; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.concurrent.ExecutionException; + +import javax.inject.Inject; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatActivity; +import androidx.camera.camera2.interop.Camera2Interop; +import androidx.camera.core.AspectRatio; +import androidx.camera.core.Camera; +import androidx.camera.core.ImageCapture; +import androidx.camera.core.ImageCaptureException; +import androidx.camera.core.Preview; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.core.content.ContextCompat; +import androidx.exifinterface.media.ExifInterface; +import androidx.lifecycle.ViewModelProvider; +import autodagger.AutoInjector; + +import static com.nextcloud.talk.utils.Mimetype.IMAGE_JPEG; + +@AutoInjector(NextcloudTalkApplication.class) +public class TakePhotoActivity extends AppCompatActivity { + private static final String TAG = TakePhotoActivity.class.getSimpleName(); + + private static final float MAX_SCALE = 6.0f; + private static final float MEDIUM_SCALE = 2.45f; + + private ActivityTakePictureBinding binding; + private TakePictureViewModel viewModel; + + private ListenableFuture cameraProviderFuture; + private OrientationEventListener orientationEventListener; + + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss", Locale.ROOT); + + private Camera camera; + + @Inject + ViewThemeUtils viewThemeUtils; + + private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + Uri uri = (Uri) binding.photoPreview.getTag(); + + if (uri != null) { + File photoFile = new File(uri.getPath()); + if (!photoFile.delete()) { + Log.w(TAG, "Error deleting temp camera image"); + } + binding.photoPreview.setTag(null); + } + + finish(); + } + }; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + + binding = ActivityTakePictureBinding.inflate(getLayoutInflater()); + viewModel = new ViewModelProvider(this).get(TakePictureViewModel.class); + + setContentView(binding.getRoot()); + + viewThemeUtils.material.themeFAB(binding.takePhoto); + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.send); + + cameraProviderFuture = ProcessCameraProvider.getInstance(this); + cameraProviderFuture.addListener(() -> { + try { + final ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); + + camera = cameraProvider.bindToLifecycle( + this, + viewModel.getCameraSelector(), + getImageCapture( + viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()), + getPreview(viewModel.isCropEnabled().getValue())); + + viewModel.getTorchToggleButtonImageResource() + .observe( + this, + res -> binding.toggleTorch.setIcon(ContextCompat.getDrawable(this, res))); + viewModel.isTorchEnabled() + .observe( + this, + enabled -> camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue())); + binding.toggleTorch.setOnClickListener((v) -> viewModel.toggleTorchEnabled()); + + viewModel.getCropToggleButtonImageResource() + .observe( + this, + res -> binding.toggleCrop.setIcon(ContextCompat.getDrawable(this, res))); + viewModel.isCropEnabled() + .observe( + this, + enabled -> { + cameraProvider.unbindAll(); + camera = cameraProvider.bindToLifecycle( + this, + viewModel.getCameraSelector(), + getImageCapture( + viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()), + getPreview(viewModel.isCropEnabled().getValue())); + camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue()); + }); + binding.toggleCrop.setOnClickListener((v) -> viewModel.toggleCropEnabled()); + + viewModel.getLowResolutionToggleButtonImageResource() + .observe( + this, + res -> binding.toggleLowres.setIcon(ContextCompat.getDrawable(this, res))); + viewModel.isLowResolutionEnabled() + .observe( + this, + enabled -> { + cameraProvider.unbindAll(); + camera = cameraProvider.bindToLifecycle( + this, + viewModel.getCameraSelector(), + getImageCapture( + viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()), + getPreview(viewModel.isCropEnabled().getValue())); + camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue()); + }); + binding.toggleLowres.setOnClickListener((v) -> viewModel.toggleLowResolutionEnabled()); + + binding.switchCamera.setOnClickListener((v) -> { + viewModel.toggleCameraSelector(); + cameraProvider.unbindAll(); + camera = cameraProvider.bindToLifecycle( + this, + viewModel.getCameraSelector(), + getImageCapture( + viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()), + getPreview(viewModel.isCropEnabled().getValue())); + }); + binding.retake.setOnClickListener((v) -> { + Uri uri = (Uri) binding.photoPreview.getTag(); + File photoFile = new File(uri.getPath()); + if (!photoFile.delete()) { + Log.w(TAG, "Error deleting temp camera image"); + } + binding.takePhoto.setEnabled(true); + binding.photoPreview.setTag(null); + showCameraElements(); + }); + binding.send.setOnClickListener((v) -> { + Uri uri = (Uri) binding.photoPreview.getTag(); + setResult(RESULT_OK, new Intent().setDataAndType(uri, IMAGE_JPEG)); + binding.photoPreview.setTag(null); + finish(); + }); + + ScaleGestureDetector mDetector = + new ScaleGestureDetector(this, new ScaleGestureDetector.SimpleOnScaleGestureListener(){ + @Override + public boolean onScale(ScaleGestureDetector detector){ + float ratio = camera.getCameraInfo().getZoomState().getValue().getZoomRatio(); + float delta = detector.getScaleFactor(); + camera.getCameraControl().setZoomRatio(ratio * delta); + return true; + } + }); + binding.preview.setOnTouchListener((v, event) -> { + v.performClick(); + mDetector.onTouchEvent(event); + return true; + }); + + // Enable enlarging the image more than default 3x maximumScale. + // Medium scale adapted to make double-tap behaviour more consistent. + binding.photoPreview.setMaximumScale(MAX_SCALE); + binding.photoPreview.setMediumScale(MEDIUM_SCALE); + } catch (IllegalArgumentException | ExecutionException | InterruptedException e) { + Log.e(TAG, "Error taking picture", e); + Snackbar.make(binding.getRoot(), e.getMessage(), Snackbar.LENGTH_LONG).show(); + finish(); + } + }, ContextCompat.getMainExecutor(this)); + + getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback); + } + + private void showCameraElements() { + binding.send.setVisibility(View.GONE); + binding.retake.setVisibility(View.GONE); + binding.photoPreview.setVisibility(View.INVISIBLE); + + binding.preview.setVisibility(View.VISIBLE); + binding.takePhoto.setVisibility(View.VISIBLE); + binding.switchCamera.setVisibility(View.VISIBLE); + binding.toggleTorch.setVisibility(View.VISIBLE); + binding.toggleCrop.setVisibility(View.VISIBLE); + binding.toggleLowres.setVisibility(View.VISIBLE); + } + + private void showPictureProcessingElements() { + binding.preview.setVisibility(View.INVISIBLE); + binding.takePhoto.setVisibility(View.GONE); + binding.switchCamera.setVisibility(View.GONE); + binding.toggleTorch.setVisibility(View.GONE); + binding.toggleCrop.setVisibility(View.GONE); + binding.toggleLowres.setVisibility(View.GONE); + + binding.send.setVisibility(View.VISIBLE); + binding.retake.setVisibility(View.VISIBLE); + binding.photoPreview.setVisibility(View.VISIBLE); + } + + private ImageCapture getImageCapture(Boolean crop, Boolean lowres) { + final ImageCapture imageCapture; + if (lowres) imageCapture = new ImageCapture.Builder() + .setTargetResolution(new Size(crop ? 1080 : 1440, 1920)).build(); + else imageCapture = new ImageCapture.Builder() + .setTargetAspectRatio(crop ? AspectRatio.RATIO_16_9 : AspectRatio.RATIO_4_3).build(); + + orientationEventListener = new OrientationEventListener(this) { + @Override + public void onOrientationChanged(int orientation) { + int rotation; + + // Monitors orientation values to determine the target rotation value + if (orientation >= 45 && orientation < 135) { + rotation = Surface.ROTATION_270; + } else if (orientation >= 135 && orientation < 225) { + rotation = Surface.ROTATION_180; + } else if (orientation >= 225 && orientation < 315) { + rotation = Surface.ROTATION_90; + } else { + rotation = Surface.ROTATION_0; + } + + imageCapture.setTargetRotation(rotation); + } + }; + orientationEventListener.enable(); + + binding.takePhoto.setOnClickListener((v) -> { + binding.takePhoto.setEnabled(false); + final String photoFileName = dateFormat.format(new Date()) + ".jpg"; + try { + final File photoFile = FileUtils.getTempCacheFile(this, "photos/" + photoFileName); + final ImageCapture.OutputFileOptions options = + new ImageCapture.OutputFileOptions.Builder(photoFile).build(); + imageCapture.takePicture( + options, + ContextCompat.getMainExecutor(this), + new ImageCapture.OnImageSavedCallback() { + + @Override + public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) { + setPreviewImage(photoFile); + showPictureProcessingElements(); + } + + @Override + public void onError(@NonNull ImageCaptureException e) { + Log.e(TAG, "Error", e); + + if (!photoFile.delete()) { + Log.w(TAG, "Deleting picture failed"); + } + binding.takePhoto.setEnabled(true); + } + }); + } catch (Exception e) { + Log.e(TAG, "error while taking picture", e); + Snackbar.make(binding.getRoot(), R.string.take_photo_error_deleting_picture, Snackbar.LENGTH_SHORT).show(); + } + }); + + return imageCapture; + } + + private void setPreviewImage(File photoFile) { + final Uri savedUri = Uri.fromFile(photoFile); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); + int doubleScreenWidth = displayMetrics.widthPixels * 2; + int doubleScreenHeight = displayMetrics.heightPixels * 2; + + Bitmap bitmap = BitmapShrinker.shrinkBitmap(photoFile.getAbsolutePath(), + doubleScreenWidth, + doubleScreenHeight); + + binding.photoPreview.setImageBitmap(bitmap); + binding.photoPreview.setTag(savedUri); + viewModel.disableTorchIfEnabled(); + } + + public int getImageOrientation(File imageFile) { + int rotate = 0; + try { + ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath()); + int orientation = exif.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL); + + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_270: + rotate = 270; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + rotate = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_90: + rotate = 90; + break; + default: + rotate = 0; + break; + } + + Log.i(TAG, "ImageOrientation - Exif orientation: " + orientation + " - " + "Rotate value: " + rotate); + } catch (Exception e) { + Log.w(TAG, "Error calculation rotation value"); + } + return rotate; + } + + @OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class) + private Preview getPreview(boolean crop) { + Preview.Builder previewBuilder = new Preview.Builder() + .setTargetAspectRatio(crop ? AspectRatio.RATIO_16_9 : AspectRatio.RATIO_4_3); + new Camera2Interop.Extender<>(previewBuilder) + .setCaptureRequestOption(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, + CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF + ); + + Preview preview = previewBuilder.build(); + preview.setSurfaceProvider(binding.preview.getSurfaceProvider()); + + return preview; + } + + @Override + protected void onPause() { + if (this.orientationEventListener != null) { + this.orientationEventListener.disable(); + } + super.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + if (this.orientationEventListener != null) { + this.orientationEventListener.enable(); + } + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + if (binding.photoPreview.getTag() != null) { + savedInstanceState.putString("Uri", ((Uri) binding.photoPreview.getTag()).getPath()); + } + + super.onSaveInstanceState(savedInstanceState); + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + String uri = savedInstanceState.getString("Uri", null); + + if (uri != null) { + File photoFile = new File(uri); + setPreviewImage(photoFile); + showPictureProcessingElements(); + } + } + + public static Intent createIntent(@NonNull Context context) { + return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt new file mode 100644 index 0000000..70d714f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt @@ -0,0 +1,59 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import fr.dudie.nominatim.model.Address + +class GeocodingAdapter(private val context: Context, private var dataSource: List
) : + RecyclerView.Adapter() { + + interface OnItemClickListener { + fun onItemClick(position: Int) + } + + @SuppressLint("NotifyDataSetChanged") + fun updateData(data: List
) { + this.dataSource = data + notifyDataSetChanged() + } + + private var listener: OnItemClickListener? = null + fun setOnItemClickListener(listener: OnItemClickListener) { + this.listener = listener + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.geocoding_item, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val address = dataSource[position] + holder.nameView.text = address.displayName + + holder.itemView.setOnClickListener { + listener?.onItemClick(position) + } + } + + override fun getItemCount(): Int = dataSource.size + + fun getItem(position: Int): Any = dataSource[position] + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val nameView: TextView = itemView.findViewById(R.id.name) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt new file mode 100644 index 0000000..5fd0ed9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt @@ -0,0 +1,215 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-FileCopyrightText: 2021-2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.text.TextUtils +import android.util.Log +import android.view.ViewGroup +import com.nextcloud.talk.call.CallParticipantModel +import com.nextcloud.talk.call.RaisedHand +import com.nextcloud.talk.models.json.participants.Participant.ActorType +import com.nextcloud.talk.utils.ApiUtils.getUrlForAvatar +import com.nextcloud.talk.utils.ApiUtils.getUrlForFederatedAvatar +import com.nextcloud.talk.utils.ApiUtils.getUrlForGuestAvatar +import com.nextcloud.talk.utils.DisplayUtils.isDarkModeOn +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.webrtc.EglBase +import org.webrtc.MediaStream +import org.webrtc.PeerConnection.IceConnectionState +import org.webrtc.SurfaceViewRenderer + +data class ParticipantUiState( + val sessionKey: String, + val nick: String, + val isConnected: Boolean, + val isAudioEnabled: Boolean, + val isStreamEnabled: Boolean, + val raisedHand: Boolean, + val avatarUrl: String?, + val mediaStream: MediaStream? +) + +@Suppress("LongParameterList") +class ParticipantDisplayItem( + private val context: Context, + private val baseUrl: String, + private val defaultGuestNick: String, + val rootEglBase: EglBase, + private val streamType: String, + private val roomToken: String, + private val callParticipantModel: CallParticipantModel +) { + private val participantDisplayItemNotifier = ParticipantDisplayItemNotifier() + + private val _uiStateFlow = MutableStateFlow(buildUiState()) + val uiStateFlow: StateFlow = _uiStateFlow.asStateFlow() + + private val session: String = callParticipantModel.sessionId + + var actorType: ActorType? = null + private set + private var actorId: String? = null + private var userId: String? = null + private var iceConnectionState: IceConnectionState? = null + var nick: String? = null + get() = (if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(field)) defaultGuestNick else field) + + var urlForAvatar: String? = null + private set + var mediaStream: MediaStream? = null + private set + var isStreamEnabled: Boolean = false + private set + var isAudioEnabled: Boolean = false + private set + var raisedHand: RaisedHand? = null + private set + var surfaceViewRenderer: SurfaceViewRenderer? = null + + val sessionKey: String + get() = "$session-$streamType" + + interface Observer { + fun onChange() + } + + private val callParticipantModelObserver: CallParticipantModel.Observer = object : CallParticipantModel.Observer { + override fun onChange() { + updateFromModel() + } + + override fun onReaction(reaction: String) { + // unused + } + } + + init { + callParticipantModel.addObserver(callParticipantModelObserver, handler) + + updateFromModel() + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun destroy() { + callParticipantModel.removeObserver(callParticipantModelObserver) + + surfaceViewRenderer?.let { renderer -> + try { + mediaStream?.videoTracks?.firstOrNull()?.removeSink(renderer) + renderer.clearImage() + renderer.release() + (renderer.parent as? ViewGroup)?.removeView(renderer) + } catch (e: Exception) { + Log.w("ParticipantDisplayItem", "Error releasing renderer", e) + } + } + surfaceViewRenderer = null + } + + private fun updateFromModel() { + actorType = callParticipantModel.actorType + actorId = callParticipantModel.actorId + userId = callParticipantModel.userId + nick = callParticipantModel.nick + + updateUrlForAvatar() + + if (streamType == "screen") { + iceConnectionState = callParticipantModel.screenIceConnectionState + mediaStream = callParticipantModel.screenMediaStream + isAudioEnabled = true + isStreamEnabled = true + } else { + iceConnectionState = callParticipantModel.iceConnectionState + mediaStream = callParticipantModel.mediaStream + isAudioEnabled = callParticipantModel.isAudioAvailable ?: false + isStreamEnabled = callParticipantModel.isVideoAvailable ?: false + } + + raisedHand = callParticipantModel.raisedHand + + if (surfaceViewRenderer == null && mediaStream != null) { + val renderer = SurfaceViewRenderer(context).apply { + init(rootEglBase.eglBaseContext, null) + setEnableHardwareScaler(true) + setMirror(false) + } + surfaceViewRenderer = renderer + mediaStream?.videoTracks?.firstOrNull()?.addSink(renderer) + } + + _uiStateFlow.value = buildUiState() + participantDisplayItemNotifier.notifyChange() + } + + private fun buildUiState(): ParticipantUiState = + ParticipantUiState( + sessionKey = sessionKey, + nick = nick ?: "Guest", + isConnected = isConnected, + isAudioEnabled = isAudioEnabled, + isStreamEnabled = isStreamEnabled, + raisedHand = raisedHand?.state == true, + avatarUrl = urlForAvatar, + mediaStream = mediaStream + ) + + private fun updateUrlForAvatar() { + if (actorType == ActorType.FEDERATED) { + val darkTheme = if (isDarkModeOn(context)) 1 else 0 + urlForAvatar = getUrlForFederatedAvatar(baseUrl, roomToken, actorId!!, darkTheme, true) + } else if (!TextUtils.isEmpty(userId)) { + urlForAvatar = getUrlForAvatar(baseUrl, userId, true) + } else { + urlForAvatar = getUrlForGuestAvatar(baseUrl, nick, true) + } + } + + val isConnected: Boolean + get() = iceConnectionState == IceConnectionState.CONNECTED || + iceConnectionState == IceConnectionState.COMPLETED || + // If there is no connection state that means that no connection is needed, + // so it is a special case that is also seen as "connected". + iceConnectionState == null + + fun addObserver(observer: Observer?) { + participantDisplayItemNotifier.addObserver(observer) + } + + fun removeObserver(observer: Observer?) { + participantDisplayItemNotifier.removeObserver(observer) + } + + override fun toString(): String = + "ParticipantSession{" + + "userId='" + userId + '\'' + + ", actorType='" + actorType + '\'' + + ", actorId='" + actorId + '\'' + + ", session='" + session + '\'' + + ", nick='" + nick + '\'' + + ", urlForAvatar='" + urlForAvatar + '\'' + + ", mediaStream=" + mediaStream + + ", streamType='" + streamType + '\'' + + ", streamEnabled=" + isStreamEnabled + + ", rootEglBase=" + rootEglBase + + ", raisedHand=" + raisedHand + + '}' + + companion object { + /** + * Shared handler to receive change notifications from the model on the main thread. + */ + private val handler = Handler(Looper.getMainLooper()) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItemNotifier.java b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItemNotifier.java new file mode 100644 index 0000000..a52c019 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItemNotifier.java @@ -0,0 +1,40 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Helper class to register and notify ParticipantDisplayItem.Observers. + *

+ * This class is only meant for internal use by ParticipantDisplayItem; observers must register themselves against a + * ParticipantDisplayItem rather than against a ParticipantDisplayItemNotifier. + */ +class ParticipantDisplayItemNotifier { + + private final Set participantDisplayItemObservers = new LinkedHashSet<>(); + + public synchronized void addObserver(ParticipantDisplayItem.Observer observer) { + if (observer == null) { + throw new IllegalArgumentException("ParticipantDisplayItem.Observer can not be null"); + } + + participantDisplayItemObservers.add(observer); + } + + public synchronized void removeObserver(ParticipantDisplayItem.Observer observer) { + participantDisplayItemObservers.remove(observer); + } + + public synchronized void notifyChange() { + for (ParticipantDisplayItem.Observer observer : new ArrayList<>(participantDisplayItemObservers)) { + observer.onChange(); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusClickListener.kt b/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusClickListener.kt new file mode 100644 index 0000000..c6a5431 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusClickListener.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.databinding.PredefinedStatusBinding +import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus + +class PredefinedStatusListAdapter( + private val clickListener: PredefinedStatusClickListener, + val context: Context, + var isBackupStatusAvailable: Boolean +) : RecyclerView.Adapter() { + internal var list: List = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PredefinedStatusViewHolder { + val itemBinding = PredefinedStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return PredefinedStatusViewHolder(itemBinding) + } + + override fun onBindViewHolder(holder: PredefinedStatusViewHolder, position: Int) { + holder.bind(list[position], clickListener, context, isBackupStatusAvailable) + } + + override fun getItemCount(): Int = list.size +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusViewHolder.kt new file mode 100644 index 0000000..cad4327 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/PredefinedStatusViewHolder.kt @@ -0,0 +1,60 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.databinding.PredefinedStatusBinding +import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus +import com.nextcloud.talk.utils.DisplayUtils + +private const val ONE_SECOND_IN_MILLIS = 1000 + +@Suppress("DEPRECATION") +class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : RecyclerView.ViewHolder(binding.root) { + + fun bind( + status: PredefinedStatus, + clickListener: PredefinedStatusClickListener, + context: Context, + isBackupStatusAvailable: Boolean + ) { + binding.root.setOnClickListener { clickListener.onClick(status) } + binding.icon.text = status.icon + binding.name.text = status.message + + if (status.clearAt == null) { + binding.clearAt.text = context.getString(R.string.dontClear) + } else { + val clearAt = status.clearAt!! + if (clearAt.type.equals("period")) { + binding.clearAt.text = DisplayUtils.getRelativeTimestamp( + context, + System.currentTimeMillis() + clearAt.time.toInt() * ONE_SECOND_IN_MILLIS, + true + ) + } else { + // end-of + if (clearAt.time.equals("day")) { + binding.clearAt.text = context.getString(R.string.today) + } + } + } + if (isBackupStatusAvailable) { + binding.resetStatusButton.visibility = if (position == 0) View.VISIBLE else View.GONE + if (position == 0) { + binding.clearAt.text = context.getString(R.string.previously_set) + } + binding.resetStatusButton.setOnClickListener { + clickListener.revertStatus() + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ReactionItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/ReactionItem.kt new file mode 100644 index 0000000..d1e496c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/ReactionItem.kt @@ -0,0 +1,11 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters + +import com.nextcloud.talk.models.json.reactions.ReactionVoter + +data class ReactionItem(val reactionVoter: ReactionVoter, val reaction: String?) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ReactionItemClickListener.kt b/app/src/main/java/com/nextcloud/talk/adapters/ReactionItemClickListener.kt new file mode 100644 index 0000000..02357b1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/ReactionItemClickListener.kt @@ -0,0 +1,11 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters + +interface ReactionItemClickListener { + fun onClick(reactionItem: ReactionItem) +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt new file mode 100644 index 0000000..5f35a97 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ReactionItemBinding + +class ReactionsAdapter(private val clickListener: ReactionItemClickListener, private val user: User?) : + RecyclerView.Adapter() { + internal var list: MutableList = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReactionsViewHolder { + val itemBinding = ReactionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ReactionsViewHolder(itemBinding, user) + } + + override fun onBindViewHolder(holder: ReactionsViewHolder, position: Int) { + holder.bind(list[position], clickListener) + } + + override fun getItemCount(): Int = list.size +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt new file mode 100644 index 0000000..43b12e6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt @@ -0,0 +1,48 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters + +import android.text.TextUtils +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ReactionItemBinding +import com.nextcloud.talk.extensions.loadGuestAvatar +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.reactions.ReactionVoter + +class ReactionsViewHolder(private val binding: ReactionItemBinding, private val user: User?) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(reactionItem: ReactionItem, clickListener: ReactionItemClickListener) { + binding.root.setOnClickListener { clickListener.onClick(reactionItem) } + binding.reaction.text = reactionItem.reaction + binding.name.text = reactionItem.reactionVoter.actorDisplayName + + if (user != null && user.baseUrl?.isNotEmpty() == true) { + loadAvatar(reactionItem) + } + } + + private fun loadAvatar(reactionItem: ReactionItem) { + if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.GUESTS) { + var displayName = sharedApplication?.resources?.getString(R.string.nc_guest) + if (!TextUtils.isEmpty(reactionItem.reactionVoter.actorDisplayName)) { + displayName = reactionItem.reactionVoter.actorDisplayName!! + } + binding.avatar.loadGuestAvatar(user!!.baseUrl!!, displayName!!, false) + } else if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.USERS) { + binding.avatar.loadUserAvatar( + user!!, + reactionItem.reactionVoter.actorId!!, + false, + false + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.kt new file mode 100644 index 0000000..30e4f16 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.kt @@ -0,0 +1,109 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.accounts.Account +import android.text.TextUtils +import android.view.View +import androidx.core.net.toUri +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.items.AdvancedUserItem.UserItemViewHolder +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.AccountItemBinding +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFilterable +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import java.util.regex.Pattern + +class AdvancedUserItem( + /** + * @return the model object + */ + val model: Participant, + @JvmField val user: User?, + val account: Account?, + private val viewThemeUtils: ViewThemeUtils, + private val actionRequiredCount: Int +) : AbstractFlexibleItem(), + IFilterable { + + override fun equals(other: Any?): Boolean = + if (other is AdvancedUserItem) { + model == other.model + } else { + false + } + + override fun hashCode(): Int = model.hashCode() + + override fun getLayoutRes(): Int = R.layout.account_item + + override fun createViewHolder( + view: View?, + adapter: FlexibleAdapter>? + ): UserItemViewHolder = UserItemViewHolder(view, adapter) + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: UserItemViewHolder, + position: Int, + payloads: MutableList + ) { + if (adapter.hasFilter()) { + viewThemeUtils.talk.themeAndHighlightText( + holder.binding.userName, + model.displayName, + adapter.getFilter(String::class.java).toString() + ) + } else { + holder.binding.userName.text = model.displayName + } + if (user != null && !TextUtils.isEmpty(user.baseUrl)) { + val host = user.baseUrl!!.toUri().host + if (!TextUtils.isEmpty(host)) { + holder.binding.account.text = user.baseUrl!!.toUri().host + } else { + holder.binding.account.text = user.baseUrl + } + } + if (user?.baseUrl != null && + (user.baseUrl!!.startsWith("http://") || user.baseUrl!!.startsWith("https://")) + ) { + holder.binding.userIcon.loadUserAvatar(user, model.calculatedActorId!!, true, false) + } + if (actionRequiredCount > 0) { + holder.binding.actionRequired.visibility = View.VISIBLE + } else { + holder.binding.actionRequired.visibility = View.GONE + } + } + + override fun filter(constraint: String?): Boolean = + model.displayName != null && + Pattern + .compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(model.displayName!!.trim()) + .find() + + class UserItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) { + var binding: AccountItemBinding + + /** + * Default constructor. + */ + init { + binding = AccountItemBinding.bind(view!!) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.kt new file mode 100644 index 0000000..815aff1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.kt @@ -0,0 +1,192 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.text.TextUtils +import android.view.View +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.items.ContactItem.ContactItemViewHolder +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.RvItemContactBinding +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFilterable +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.flexibleadapter.items.ISectionable +import eu.davidea.viewholders.FlexibleViewHolder +import java.util.Objects +import java.util.regex.Pattern + +class ContactItem( + /** + * @return the model object + */ + val model: Participant, + private val user: User, + private var header: GenericTextHeaderItem?, + private val viewThemeUtils: ViewThemeUtils +) : AbstractFlexibleItem(), + ISectionable, + IFilterable { + var isOnline: Boolean = true + + override fun equals(o: Any?): Boolean { + if (o is ContactItem) { + return model.calculatedActorType == o.model.calculatedActorType && + model.calculatedActorId == o.model.calculatedActorId + } + return false + } + override fun hashCode(): Int = model.hashCode() + + override fun filter(constraint: String?): Boolean = + model.displayName != null && + ( + Pattern.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(model.displayName!!.trim()) + .find() || + Pattern.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(model.calculatedActorId!!.trim()) + .find() + ) + + override fun getLayoutRes(): Int = R.layout.rv_item_contact + + override fun createViewHolder( + view: View?, + adapter: FlexibleAdapter>? + ): ContactItemViewHolder = ContactItemViewHolder(view, adapter) + + override fun bindViewHolder( + adapter: FlexibleAdapter>?, + holder: ContactItemViewHolder?, + position: Int, + payloads: List? + ) { + if (model.selected) { + holder?.binding?.checkedImageView?.let { viewThemeUtils.platform.colorImageView(it) } + holder?.binding?.checkedImageView?.visibility = View.VISIBLE + } else { + holder?.binding?.checkedImageView?.visibility = View.GONE + } + + if (!isOnline) { + holder?.binding?.nameText?.setTextColor( + ResourcesCompat.getColor( + holder.binding.nameText.context.resources, + R.color.medium_emphasis_text, + null + ) + ) + holder?.binding?.avatarView?.alpha = SEMI_TRANSPARENT + } else { + holder?.binding?.nameText?.setTextColor( + ResourcesCompat.getColor( + holder.binding.nameText.context.resources, + R.color.high_emphasis_text, + null + ) + ) + holder?.binding?.avatarView?.alpha = FULLY_OPAQUE + } + + holder?.binding?.nameText?.text = model.displayName + + if (adapter != null) { + if (adapter.hasFilter()) { + holder?.binding?.let { + viewThemeUtils.talk.themeAndHighlightText( + it.nameText, + model.displayName, + adapter.getFilter(String::class.java).toString() + ) + } + } + } + + if (TextUtils.isEmpty(model.displayName) && + ( + model.type == Participant.ParticipantType.GUEST || + model.type == Participant.ParticipantType.USER_FOLLOWING_LINK + ) + ) { + holder?.binding?.nameText?.text = sharedApplication!!.getString(R.string.nc_guest) + } + + setAvatar(holder) + } + + private fun setAvatar(holder: ContactItemViewHolder?) { + if (model.calculatedActorType == Participant.ActorType.GROUPS || + model.calculatedActorType == Participant.ActorType.CIRCLES + ) { + setGenericAvatar(holder!!, R.drawable.ic_avatar_group) + } else if (model.calculatedActorType == Participant.ActorType.EMAILS) { + setGenericAvatar(holder!!, R.drawable.ic_avatar_mail) + } else if (model.calculatedActorType == Participant.ActorType.GUESTS || + model.type == Participant.ParticipantType.GUEST || + model.type == Participant.ParticipantType.GUEST_MODERATOR + ) { + var displayName: String? + + displayName = if (!TextUtils.isEmpty(model.displayName)) { + model.displayName + } else { + Objects.requireNonNull(sharedApplication)!!.resources!!.getString(R.string.nc_guest) + } + + // absolute fallback to prevent NPE deference + if (displayName == null) { + displayName = "Guest" + } + + holder?.binding?.avatarView?.loadUserAvatar(user, displayName, true, false) + } else if (model.calculatedActorType == Participant.ActorType.USERS) { + holder?.binding?.avatarView + ?.loadUserAvatar( + user, + model.calculatedActorId!!, + true, + false + ) + } + } + + private fun setGenericAvatar(holder: ContactItemViewHolder, roundPlaceholderDrawable: Int) { + val avatar = + viewThemeUtils.talk.themePlaceholderAvatar( + holder.binding.avatarView, + roundPlaceholderDrawable + ) + + holder.binding.avatarView.loadUserAvatar(avatar) + } + + override fun getHeader(): GenericTextHeaderItem? = header + + override fun setHeader(p0: GenericTextHeaderItem?) { + this.header = header + } + + class ContactItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) { + var binding: RvItemContactBinding = + RvItemContactBinding.bind(view!!) + } + + companion object { + private const val FULLY_OPAQUE: Float = 1.0f + private const val SEMI_TRANSPARENT: Float = 0.38f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt new file mode 100644 index 0000000..745070b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt @@ -0,0 +1,507 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Typeface +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextUtils +import android.text.format.DateUtils +import android.text.style.ImageSpan +import android.view.View +import android.widget.RelativeLayout +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import coil.dispose +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.items.ConversationItem.ConversationItemViewHolder +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage.MessageType +import com.nextcloud.talk.data.database.mappers.asModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.RvItemConversationWithLastMessageBinding +import com.nextcloud.talk.extensions.loadConversationAvatar +import com.nextcloud.talk.extensions.loadNoteToSelfAvatar +import com.nextcloud.talk.extensions.loadSystemAvatar +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.ui.StatusDrawable +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.SpreedFeatures +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFilterable +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.flexibleadapter.items.ISectionable +import eu.davidea.viewholders.FlexibleViewHolder +import java.util.regex.Pattern + +class ConversationItem( + val model: ConversationModel, + private val user: User, + private val context: Context, + private val viewThemeUtils: ViewThemeUtils +) : AbstractFlexibleItem(), + ISectionable, + IFilterable { + private var header: GenericTextHeaderItem? = null + private val chatMessage = model.lastMessage?.asModel() + var mHolder: ConversationItemViewHolder? = null + + constructor( + conversation: ConversationModel, + user: User, + activityContext: Context, + genericTextHeaderItem: GenericTextHeaderItem?, + viewThemeUtils: ViewThemeUtils + ) : this(conversation, user, activityContext, viewThemeUtils) { + header = genericTextHeaderItem + } + + override fun equals(other: Any?): Boolean { + if (other is ConversationItem) { + return model == other.model + } + return false + } + + override fun hashCode(): Int { + var result = model.hashCode() + result *= 31 + return result + } + + override fun getLayoutRes(): Int = R.layout.rv_item_conversation_with_last_message + + override fun getItemViewType(): Int = VIEW_TYPE + + override fun createViewHolder(view: View, adapter: FlexibleAdapter?>?): ConversationItemViewHolder = + ConversationItemViewHolder(view, adapter) + + @SuppressLint("SetTextI18n") + override fun bindViewHolder( + adapter: FlexibleAdapter?>, + holder: ConversationItemViewHolder, + position: Int, + payloads: List + ) { + mHolder = holder + val appContext = sharedApplication!!.applicationContext + holder.binding.dialogName.setTextColor( + ResourcesCompat.getColor( + context.resources, + R.color.conversation_item_header, + null + ) + ) + if (adapter.hasFilter()) { + viewThemeUtils.platform.highlightText( + holder.binding.dialogName, + model.displayName!!, + adapter.getFilter(String::class.java).toString() + ) + } else { + holder.binding.dialogName.text = model.displayName + } + if (model.unreadMessages > 0) { + showUnreadMessages(holder) + } else { + holder.binding.dialogName.setTypeface(null, Typeface.NORMAL) + holder.binding.dialogDate.setTypeface(null, Typeface.NORMAL) + holder.binding.dialogLastMessage.setTypeface(null, Typeface.NORMAL) + holder.binding.dialogUnreadBubble.visibility = View.GONE + } + if (model.favorite) { + holder.binding.favoriteConversationImageView.visibility = View.VISIBLE + } else { + holder.binding.favoriteConversationImageView.visibility = View.GONE + } + if (ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == model.type) { + holder.binding.publicCallBadge.setImageResource(R.drawable.ic_avatar_link) + holder.binding.publicCallBadge.visibility = View.VISIBLE + } else if (model.remoteServer?.isNotEmpty() == true) { + holder.binding.publicCallBadge.setImageResource(R.drawable.ic_avatar_federation) + holder.binding.publicCallBadge.visibility = View.VISIBLE + } else { + holder.binding.publicCallBadge.visibility = View.GONE + } + if (ConversationEnums.ConversationType.ROOM_SYSTEM !== model.type) { + val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, appContext) + holder.binding.userStatusImage.visibility = View.VISIBLE + holder.binding.userStatusImage.setImageDrawable( + StatusDrawable( + model.status, + model.statusIcon, + size, + context.resources.getColor(R.color.bg_default, null), + appContext + ) + ) + } else { + holder.binding.userStatusImage.visibility = View.GONE + } + + val dialogNameParams = holder.binding.dialogName.layoutParams as RelativeLayout.LayoutParams + val unreadBubbleParams = holder.binding.dialogUnreadBubble.layoutParams as RelativeLayout.LayoutParams + val relativeLayoutParams = holder.binding.relativeLayout.layoutParams as RelativeLayout.LayoutParams + + if (model.hasSensitive == true) { + dialogNameParams.addRule(RelativeLayout.CENTER_VERTICAL) + relativeLayoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.dialogAvatarFrameLayout) + dialogNameParams.marginEnd = + context.resources.getDimensionPixelSize(R.dimen.standard_double_padding) + unreadBubbleParams.topMargin = + context.resources.getDimensionPixelSize(R.dimen.double_margin_between_elements) + unreadBubbleParams.addRule(RelativeLayout.CENTER_VERTICAL) + } else { + dialogNameParams.removeRule(RelativeLayout.CENTER_VERTICAL) + relativeLayoutParams.removeRule(RelativeLayout.ALIGN_TOP) + dialogNameParams.marginEnd = 0 + unreadBubbleParams.topMargin = 0 + unreadBubbleParams.removeRule(RelativeLayout.CENTER_VERTICAL) + } + holder.binding.relativeLayout.layoutParams = relativeLayoutParams + holder.binding.dialogUnreadBubble.layoutParams = unreadBubbleParams + holder.binding.dialogName.layoutParams = dialogNameParams + + setLastMessage(holder, appContext) + showAvatar(holder) + } + + private fun showAvatar(holder: ConversationItemViewHolder) { + holder.binding.dialogAvatar.dispose() + holder.binding.dialogAvatar.visibility = View.VISIBLE + + var shouldLoadAvatar = shouldLoadAvatar(holder) + if (ConversationEnums.ConversationType.ROOM_SYSTEM == model.type) { + holder.binding.dialogAvatar.loadSystemAvatar() + shouldLoadAvatar = false + } + if (shouldLoadAvatar) { + when (model.type) { + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> { + if (!TextUtils.isEmpty(model.name)) { + holder.binding.dialogAvatar.loadUserAvatar( + user, + model.name!!, + true, + false + ) + } else { + holder.binding.dialogAvatar.visibility = View.GONE + } + } + + ConversationEnums.ConversationType.ROOM_GROUP_CALL, + ConversationEnums.ConversationType.FORMER_ONE_TO_ONE, + ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> + holder.binding.dialogAvatar.loadConversationAvatar(user, model, false, viewThemeUtils) + + ConversationEnums.ConversationType.NOTE_TO_SELF -> + holder.binding.dialogAvatar.loadNoteToSelfAvatar() + + else -> holder.binding.dialogAvatar.visibility = View.GONE + } + } + } + + private fun shouldLoadAvatar(holder: ConversationItemViewHolder): Boolean = + when (model.objectType) { + ConversationEnums.ObjectType.SHARE_PASSWORD -> { + holder.binding.dialogAvatar.setImageDrawable( + ContextCompat.getDrawable( + context, + R.drawable.ic_circular_lock + ) + ) + false + } + + ConversationEnums.ObjectType.FILE -> { + holder.binding.dialogAvatar.loadUserAvatar( + viewThemeUtils.talk.themePlaceholderAvatar( + holder.binding.dialogAvatar, + R.drawable.ic_avatar_document + ) + ) + false + } + + else -> true + } + + private fun setLastMessage(holder: ConversationItemViewHolder, appContext: Context) { + if (chatMessage != null) { + holder.binding.dialogDate.visibility = View.VISIBLE + holder.binding.dialogDate.text = DateUtils.getRelativeTimeSpanString( + model.lastActivity * MILLIES, + System.currentTimeMillis(), + 0, + DateUtils.FORMAT_ABBREV_RELATIVE + ) + if (!TextUtils.isEmpty(chatMessage?.systemMessage) || + ConversationEnums.ConversationType.ROOM_SYSTEM === model.type + ) { + holder.binding.dialogLastMessage.text = chatMessage.text + } else { + chatMessage?.activeUser = user + + val text = + if ( + chatMessage?.getCalculateMessageType() == MessageType.REGULAR_TEXT_MESSAGE + ) { + calculateRegularLastMessageText(appContext) + } else { + lastMessageDisplayText + } + holder.binding.dialogLastMessage.text = text + } + } else { + holder.binding.dialogDate.visibility = View.GONE + holder.binding.dialogLastMessage.text = "" + } + } + + private fun calculateRegularLastMessageText(appContext: Context): CharSequence = + if (chatMessage?.actorId == user.userId) { + String.format( + appContext.getString(R.string.nc_formatted_message_you), + lastMessageDisplayText + ) + } else if (model.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + lastMessageDisplayText + } else { + val actorName = chatMessage?.actorDisplayName + val authorDisplayName = if (!actorName.isNullOrBlank()) { + actorName + } else if ("guests" == chatMessage?.actorType || "emails" == chatMessage?.actorType) { + appContext.getString(R.string.nc_guest) + } else { + "" + } + + String.format( + appContext.getString(R.string.nc_formatted_message), + authorDisplayName, + lastMessageDisplayText + ) + } + + private fun showUnreadMessages(holder: ConversationItemViewHolder) { + holder.binding.dialogName.setTypeface(holder.binding.dialogName.typeface, Typeface.BOLD) + holder.binding.dialogLastMessage.setTypeface(holder.binding.dialogLastMessage.typeface, Typeface.BOLD) + holder.binding.dialogUnreadBubble.visibility = View.VISIBLE + if (model.unreadMessages < UNREAD_MESSAGES_TRESHOLD) { + holder.binding.dialogUnreadBubble.text = model.unreadMessages.toLong().toString() + } else { + holder.binding.dialogUnreadBubble.setText(R.string.tooManyUnreadMessages) + } + val lightBubbleFillColor = ColorStateList.valueOf( + ContextCompat.getColor( + context, + R.color.conversation_unread_bubble + ) + ) + val lightBubbleTextColor = ContextCompat.getColor( + context, + R.color.conversation_unread_bubble_text + ) + if (model.type === ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble) + } else if (model.unreadMention) { + if (hasSpreedFeatureCapability(user.capabilities?.spreedCapability!!, SpreedFeatures.DIRECT_MENTION_FLAG)) { + if (model.unreadMentionDirect!!) { + viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble) + } else { + viewThemeUtils.material.colorChipOutlined( + holder.binding.dialogUnreadBubble, + UNREAD_BUBBLE_STROKE_WIDTH + ) + } + } else { + viewThemeUtils.material.colorChipBackground(holder.binding.dialogUnreadBubble) + } + } else { + holder.binding.dialogUnreadBubble.chipBackgroundColor = lightBubbleFillColor + holder.binding.dialogUnreadBubble.setTextColor(lightBubbleTextColor) + } + } + + override fun filter(constraint: String?): Boolean = + model.displayName != null && + Pattern + .compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(model.displayName.trim()) + .find() + + override fun getHeader(): GenericTextHeaderItem? = header + + override fun setHeader(header: GenericTextHeaderItem?) { + this.header = header + } + + private val lastMessageDisplayText: CharSequence + get() { + if (chatMessage?.getCalculateMessageType() == MessageType.REGULAR_TEXT_MESSAGE || + chatMessage?.getCalculateMessageType() == MessageType.SYSTEM_MESSAGE || + chatMessage?.getCalculateMessageType() == MessageType.SINGLE_LINK_MESSAGE + ) { + return chatMessage.text + } else { + if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == chatMessage?.getCalculateMessageType() || + MessageType.SINGLE_LINK_TENOR_MESSAGE == chatMessage?.getCalculateMessageType() || + MessageType.SINGLE_LINK_GIF_MESSAGE == chatMessage?.getCalculateMessageType() + ) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_a_gif_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_a_gif), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == chatMessage?.getCalculateMessageType()) { + var locationName = chatMessage.messageParameters?.get("object")?.get("name") ?: "" + val author = authorName(chatMessage) + val lastMessage = + setLastNameForAttachmentMessage(author, R.drawable.baseline_location_pin_24, locationName) + return lastMessage + } else if (MessageType.VOICE_MESSAGE == chatMessage?.getCalculateMessageType()) { + var voiceMessageName = chatMessage.messageParameters?.get("file")?.get("name") ?: "" + val author = authorName(chatMessage) + val lastMessage = setLastNameForAttachmentMessage( + author, + R.drawable.baseline_mic_24, + voiceMessageName + ) + return lastMessage + } else if (MessageType.SINGLE_LINK_AUDIO_MESSAGE == chatMessage?.getCalculateMessageType()) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_an_audio_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_an_audio), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == chatMessage?.getCalculateMessageType()) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_a_video_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_a_video), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == chatMessage?.getCalculateMessageType()) { + return if (chatMessage?.actorId == chatMessage?.activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_an_image_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_an_image), + chatMessage?.getNullsafeActorDisplayName() + ) + } + } else if (MessageType.POLL_MESSAGE == chatMessage?.getCalculateMessageType()) { + var pollMessageTitle = chatMessage.messageParameters?.get("object")?.get("name") ?: "" + val author = authorName(chatMessage) + val lastMessage = setLastNameForAttachmentMessage( + author, + R.drawable.baseline_bar_chart_24, + pollMessageTitle + ) + return lastMessage + } else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == chatMessage?.getCalculateMessageType()) { + var attachmentName = chatMessage.text + if (attachmentName == "{file}") { + attachmentName = chatMessage.messageParameters?.get("file")?.get("name") ?: "" + } + val author = authorName(chatMessage) + + val drawable = chatMessage.messageParameters?.get("file")?.get("mimetype")?.let { + when { + it.contains("image") -> R.drawable.baseline_image_24 + it.contains("video") -> R.drawable.baseline_video_24 + it.contains("application") -> R.drawable.baseline_insert_drive_file_24 + it.contains("audio") -> R.drawable.baseline_audiotrack_24 + it.contains("text/vcard") -> R.drawable.baseline_contacts_24 + else -> null + } + } + val lastMessage = setLastNameForAttachmentMessage(author, drawable, attachmentName!!) + return lastMessage + } else if (MessageType.DECK_CARD == chatMessage?.getCalculateMessageType()) { + var deckTitle = chatMessage.messageParameters?.get("object")?.get("name") ?: "" + val author = authorName(chatMessage) + val lastMessage = setLastNameForAttachmentMessage(author, R.drawable.baseline_article_24, deckTitle) + return lastMessage + } + } + return "" + } + + fun authorName(chatMessage: ChatMessage): String { + val name = if (chatMessage.actorId == chatMessage.activeUser!!.userId) { + sharedApplication!!.resources.getString(R.string.nc_current_user) + } else { + chatMessage.getNullsafeActorDisplayName()?.let { "$it:" } ?: "" + } + return name + } + + fun setLastNameForAttachmentMessage(actor: String, icon: Int?, attachmentName: String): SpannableStringBuilder { + val builder = SpannableStringBuilder() + builder.append(actor) + + val drawable = icon?.let { it -> ContextCompat.getDrawable(context, it) } + if (drawable != null) { + viewThemeUtils.platform.colorDrawable( + drawable, + context.resources.getColor(R.color.low_emphasis_text, null) + ) + val desiredWidth = (drawable.intrinsicWidth * IMAGE_SCALE_FACTOR).toInt() + val desiredHeight = (drawable.intrinsicHeight * IMAGE_SCALE_FACTOR).toInt() + drawable.setBounds(0, 0, desiredWidth, desiredHeight) + + val imageSpan = ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM) + val startImage = builder.length + builder.append(" ") + builder.setSpan(imageSpan, startImage, startImage + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } else { + builder.append(" ") + } + builder.append(attachmentName) + return builder + } + + class ConversationItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) { + var binding: RvItemConversationWithLastMessageBinding + + init { + binding = RvItemConversationWithLastMessageBinding.bind(view!!) + } + } + + companion object { + const val VIEW_TYPE = FlexibleItemViewType.CONVERSATION_ITEM + private const val MILLIES = 1000L + private const val STATUS_SIZE_IN_DP = 9f + private const val UNREAD_BUBBLE_STROKE_WIDTH = 6.0f + private const val UNREAD_MESSAGES_TRESHOLD = 1000 + private const val IMAGE_SCALE_FACTOR = 0.7f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/FlexibleItemViewType.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/FlexibleItemViewType.kt new file mode 100644 index 0000000..eaf99e2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/FlexibleItemViewType.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +object FlexibleItemViewType { + const val CONVERSATION_ITEM: Int = 1120391230 + const val LOAD_MORE_RESULTS_ITEM: Int = 1120391231 + const val MESSAGE_RESULT_ITEM: Int = 1120391232 + const val MESSAGES_TEXT_HEADER_ITEM: Int = 1120391233 + const val POLL_RESULT_HEADER_ITEM: Int = 1120391234 + const val POLL_RESULT_VOTER_ITEM: Int = 1120391235 + const val POLL_RESULT_VOTERS_OVERVIEW_ITEM: Int = 1120391236 +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.kt new file mode 100644 index 0000000..a159401 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/GenericTextHeaderItem.kt @@ -0,0 +1,72 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.util.Log +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.databinding.RvItemTitleHeaderBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import java.util.Objects + +open class GenericTextHeaderItem(title: String, viewThemeUtils: ViewThemeUtils) : + AbstractHeaderItem() { + val model: String + private val viewThemeUtils: ViewThemeUtils + + init { + isHidden = false + isSelectable = false + this.model = title + this.viewThemeUtils = viewThemeUtils + } + + override fun equals(o: Any?): Boolean { + if (o is GenericTextHeaderItem) { + return model == o.model + } + return false + } + + override fun hashCode(): Int = Objects.hash(model) + + override fun getLayoutRes(): Int = R.layout.rv_item_title_header + + override fun createViewHolder( + view: View?, + adapter: FlexibleAdapter>? + ): HeaderViewHolder = HeaderViewHolder(view, adapter) + + override fun bindViewHolder( + adapter: FlexibleAdapter?>?, + holder: HeaderViewHolder, + position: Int, + payloads: List + ) { + if (payloads.size > 0) { + Log.d(TAG, "We have payloads, so ignoring!") + } else { + holder.binding.titleTextView.text = model + viewThemeUtils.platform.colorPrimaryTextViewElement(holder.binding.titleTextView) + } + } + + class HeaderViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter, true) { + var binding: RvItemTitleHeaderBinding = + RvItemTitleHeaderBinding.bind(view!!) + } + + companion object { + private const val TAG = "GenericTextHeaderItem" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt new file mode 100644 index 0000000..f47aec8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt @@ -0,0 +1,53 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.databinding.RvItemLoadMoreBinding +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFilterable +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder + +object LoadMoreResultsItem : + AbstractFlexibleItem(), + IFilterable { + + // layout is used as view type for uniqueness + const val VIEW_TYPE = FlexibleItemViewType.LOAD_MORE_RESULTS_ITEM + + class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { + var binding: RvItemLoadMoreBinding = RvItemLoadMoreBinding.bind(view) + } + + override fun getLayoutRes(): Int = R.layout.rv_item_load_more + + override fun createViewHolder( + view: View, + adapter: FlexibleAdapter> + ): ViewHolder = ViewHolder(view, adapter) + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: ViewHolder, + position: Int, + payloads: MutableList? + ) { + // nothing, it's immutable + } + + override fun filter(constraint: String?): Boolean = true + + override fun getItemViewType(): Int = VIEW_TYPE + + override fun equals(other: Any?): Boolean = other is LoadMoreResultsItem + + override fun hashCode(): Int = 0 +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt new file mode 100644 index 0000000..5de0e72 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.kt @@ -0,0 +1,264 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2021-2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.annotation.SuppressLint +import android.content.Context +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.ResourcesCompat +import com.nextcloud.talk.PhoneUtils.isPhoneNumber +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.items.ParticipantItem.ParticipantItemViewHolder +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.extensions.loadDefaultAvatar +import com.nextcloud.talk.extensions.loadFederatedUserAvatar +import com.nextcloud.talk.extensions.loadGuestAvatar +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.mention.Mention +import com.nextcloud.talk.models.json.status.StatusType +import com.nextcloud.talk.ui.StatusDrawable +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.DisplayUtils +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFilterable +import eu.davidea.flexibleadapter.items.IFlexible +import java.util.Objects +import java.util.regex.Pattern + +class MentionAutocompleteItem( + mention: Mention, + private val currentUser: User, + private val context: Context, + @JvmField val roomToken: String, + private val viewThemeUtils: ViewThemeUtils +) : AbstractFlexibleItem(), + IFilterable { + @JvmField + var source: String? + + @JvmField + val mentionId: String? + + @JvmField + val objectId: String? + + @JvmField + val displayName: String? + private val status: String? + private val statusIcon: String? + private val statusMessage: String? + + init { + mentionId = mention.mentionId + objectId = mention.id + + displayName = if (!mention.label.isNullOrBlank()) { + mention.label + } else if ("guests" == mention.source || "emails" == mention.source) { + context.resources.getString(R.string.nc_guest) + } else { + "" + } + + source = mention.source + status = mention.status + statusIcon = mention.statusIcon + statusMessage = mention.statusMessage + } + + override fun equals(o: Any?): Boolean = + if (o is MentionAutocompleteItem) { + objectId == o.objectId && displayName == o.displayName + } else { + false + } + + override fun hashCode(): Int = Objects.hash(objectId, displayName) + + override fun getLayoutRes(): Int = R.layout.rv_item_conversation_info_participant + + override fun createViewHolder(view: View, adapter: FlexibleAdapter?>?): ParticipantItemViewHolder = + ParticipantItemViewHolder(view, adapter) + + @SuppressLint("SetTextI18n") + override fun bindViewHolder( + adapter: FlexibleAdapter?>, + holder: ParticipantItemViewHolder, + position: Int, + payloads: List + ) { + holder.binding.nameText.setTextColor( + ResourcesCompat.getColor( + context.resources, + R.color.conversation_item_header, + null + ) + ) + if (adapter.hasFilter()) { + viewThemeUtils.talk.themeAndHighlightText( + holder.binding.nameText, + displayName, + adapter.getFilter(String::class.java).toString() + ) + viewThemeUtils.talk.themeAndHighlightText( + holder.binding.secondaryText, + "@$objectId", + adapter.getFilter(String::class.java).toString() + ) + } else { + holder.binding.nameText.text = displayName + } + setAvatar(holder, objectId) + drawStatus(holder) + } + + private fun setAvatar(holder: ParticipantItemViewHolder, objectId: String?) { + when (source) { + SOURCE_CALLS -> { + run { + if (isPhoneNumber(displayName)) { + holder.binding.avatarView.loadUserAvatar( + viewThemeUtils.talk.themePlaceholderAvatar( + holder.binding.avatarView, + R.drawable.ic_phone_small + ) + ) + } else { + holder.binding.avatarView.loadUserAvatar( + viewThemeUtils.talk.themePlaceholderAvatar( + holder.binding.avatarView, + R.drawable.ic_avatar_group_small + ) + ) + } + } + } + + SOURCE_GROUPS -> { + holder.binding.avatarView.loadUserAvatar( + viewThemeUtils.talk.themePlaceholderAvatar( + holder.binding.avatarView, + R.drawable + .ic_avatar_group_small + ) + ) + } + + SOURCE_FEDERATION -> { + val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0 + holder.binding.avatarView.loadFederatedUserAvatar( + currentUser, + currentUser.baseUrl!!, + roomToken, + objectId!!, + darkTheme, + requestBigSize = true, + ignoreCache = false + ) + } + + SOURCE_GUESTS, SOURCE_EMAILS -> { + if (displayName.equals(context.resources.getString(R.string.nc_guest))) { + holder.binding.avatarView.loadDefaultAvatar(viewThemeUtils) + } else { + holder.binding.avatarView.loadGuestAvatar(currentUser, displayName!!, false) + } + } + + SOURCE_TEAMS -> { + holder.binding.avatarView.loadUserAvatar( + viewThemeUtils.talk.themePlaceholderAvatar( + holder.binding.avatarView, + R.drawable + .ic_avatar_team_small + ) + ) + } + + else -> { + holder.binding.avatarView.loadUserAvatar( + currentUser, + objectId!!, + requestBigSize = true, + ignoreCache = false + ) + } + } + } + + private fun drawStatus(holder: ParticipantItemViewHolder) { + val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, context) + holder.binding.userStatusImage.setImageDrawable( + StatusDrawable( + status, + NO_ICON, + size, + context.resources.getColor(R.color.bg_default), + context + ) + ) + if (statusMessage != null) { + holder.binding.conversationInfoStatusMessage.text = statusMessage + alignUsernameVertical(holder, 0f) + } else { + holder.binding.conversationInfoStatusMessage.text = "" + alignUsernameVertical(holder, NO_USER_STATUS_DP_FROM_TOP) + } + if (!statusIcon.isNullOrEmpty()) { + holder.binding.participantStatusEmoji.setText(statusIcon) + } else { + holder.binding.participantStatusEmoji.visibility = View.GONE + } + if (status != null && status == StatusType.DND.string) { + if (statusMessage.isNullOrEmpty()) { + holder.binding.conversationInfoStatusMessage.setText(R.string.dnd) + } + } else if (status != null && status == StatusType.BUSY.string) { + if (statusMessage.isNullOrEmpty()) { + holder.binding.conversationInfoStatusMessage.setText(R.string.busy) + } + } else if (status != null && status == StatusType.AWAY.string) { + if (statusMessage.isNullOrEmpty()) { + holder.binding.conversationInfoStatusMessage.setText(R.string.away) + } + } + } + + private fun alignUsernameVertical(holder: ParticipantItemViewHolder, densityPixelsFromTop: Float) { + val layoutParams = holder.binding.nameText.layoutParams as ConstraintLayout.LayoutParams + layoutParams.topMargin = DisplayUtils.convertDpToPixel(densityPixelsFromTop, context).toInt() + holder.binding.nameText.setLayoutParams(layoutParams) + } + + override fun filter(constraint: String?): Boolean = + objectId != null && + Pattern + .compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(objectId) + .find() || + displayName != null && + Pattern + .compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(displayName) + .find() + + companion object { + private const val STATUS_SIZE_IN_DP = 9f + private const val NO_ICON = "" + private const val NO_USER_STATUS_DP_FROM_TOP: Float = 10f + const val SOURCE_CALLS = "calls" + const val SOURCE_GUESTS = "guests" + const val SOURCE_GROUPS = "groups" + const val SOURCE_EMAILS = "emails" + const val SOURCE_TEAMS = "teams" + const val SOURCE_FEDERATION = "federated_users" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt new file mode 100644 index 0000000..d217541 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt @@ -0,0 +1,89 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.RvItemSearchMessageBinding +import com.nextcloud.talk.extensions.loadThumbnail +import com.nextcloud.talk.models.domain.SearchMessageEntry +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFilterable +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.flexibleadapter.items.ISectionable +import eu.davidea.viewholders.FlexibleViewHolder + +data class MessageResultItem( + private val context: Context, + private val currentUser: User, + val messageEntry: SearchMessageEntry, + var showHeader: Boolean = false, + private val viewThemeUtils: ViewThemeUtils +) : AbstractFlexibleItem(), + IFilterable, + ISectionable { + + class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { + var binding: RvItemSearchMessageBinding + + init { + binding = RvItemSearchMessageBinding.bind(view) + } + } + + override fun getLayoutRes(): Int = R.layout.rv_item_search_message + + override fun createViewHolder( + view: View, + adapter: FlexibleAdapter> + ): ViewHolder = ViewHolder(view, adapter) + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: ViewHolder, + position: Int, + payloads: MutableList? + ) { + holder.binding.conversationTitle.text = messageEntry.title + bindMessageExcerpt(holder) + messageEntry.thumbnailURL?.let { holder.binding.thumbnail.loadThumbnail(it, currentUser) } + } + + private fun bindMessageExcerpt(holder: ViewHolder) { + viewThemeUtils.platform.highlightText( + holder.binding.messageExcerpt, + messageEntry.messageExcerpt, + messageEntry.searchTerm + ) + } + + override fun filter(constraint: String?): Boolean = true + + override fun getItemViewType(): Int = VIEW_TYPE + + companion object { + const val VIEW_TYPE = FlexibleItemViewType.MESSAGE_RESULT_ITEM + } + + override fun getHeader(): GenericTextHeaderItem = + MessagesTextHeaderItem(context, viewThemeUtils) + .apply { + isHidden = showHeader // FlexibleAdapter needs this hack for some reason + } + + override fun setHeader(header: GenericTextHeaderItem?) { + // nothing, header is always the same + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MessagesTextHeaderItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/MessagesTextHeaderItem.kt new file mode 100644 index 0000000..3751283 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MessagesTextHeaderItem.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.content.Context +import com.nextcloud.talk.R +import com.nextcloud.talk.ui.theme.ViewThemeUtils + +class MessagesTextHeaderItem(context: Context, viewThemeUtils: ViewThemeUtils) : + GenericTextHeaderItem(context.getString(R.string.messages), viewThemeUtils) { + companion object { + const val VIEW_TYPE = FlexibleItemViewType.MESSAGES_TEXT_HEADER_ITEM + } + + override fun getItemViewType(): Int = VIEW_TYPE +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/NotificationSoundItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/NotificationSoundItem.java new file mode 100644 index 0000000..a73f4ae --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/NotificationSoundItem.java @@ -0,0 +1,95 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items; + +import android.view.View; + +import com.nextcloud.talk.R; +import com.nextcloud.talk.databinding.RvItemNotificationSoundBinding; + +import java.util.List; +import java.util.Objects; + +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; +import eu.davidea.flexibleadapter.items.IFlexible; +import eu.davidea.viewholders.FlexibleViewHolder; + +public class NotificationSoundItem extends AbstractFlexibleItem { + + private final String notificationSoundName; + private final String notificationSoundUri; + + public NotificationSoundItem(String notificationSoundName, String notificationSoundUri) { + this.notificationSoundName = notificationSoundName; + this.notificationSoundUri = notificationSoundUri; + } + + public String getNotificationSoundUri() { + return notificationSoundUri; + } + + public String getNotificationSoundName() { + return notificationSoundName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + NotificationSoundItem that = (NotificationSoundItem) o; + + if (!Objects.equals(notificationSoundName, that.notificationSoundName)) { + return false; + } + return Objects.equals(notificationSoundUri, that.notificationSoundUri); + } + + @Override + public int hashCode() { + int result = notificationSoundName != null ? notificationSoundName.hashCode() : 0; + return 31 * result + (notificationSoundUri != null ? notificationSoundUri.hashCode() : 0); + } + + @Override + public int getLayoutRes() { + return R.layout.rv_item_notification_sound; + } + + @Override + public NotificationSoundItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { + return new NotificationSoundItemViewHolder(view, adapter); + } + + @Override + public void bindViewHolder(FlexibleAdapter adapter, + NotificationSoundItemViewHolder holder, + int position, + List payloads) { + holder.binding.notificationNameTextView.setText(notificationSoundName); + holder.binding.notificationNameTextView.setChecked(adapter.isSelected(position)); + } + + static class NotificationSoundItemViewHolder extends FlexibleViewHolder { + + RvItemNotificationSoundBinding binding; + + /** + * Default constructor. + */ + NotificationSoundItemViewHolder(View view, FlexibleAdapter adapter) { + super(view, adapter); + binding = RvItemNotificationSoundBinding.bind(view); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.kt new file mode 100644 index 0000000..251ec8d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.kt @@ -0,0 +1,320 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.annotation.SuppressLint +import android.content.Context +import android.text.TextUtils +import android.util.Log +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.items.ParticipantItem.ParticipantItemViewHolder +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.RvItemConversationInfoParticipantBinding +import com.nextcloud.talk.extensions.loadDefaultAvatar +import com.nextcloud.talk.extensions.loadDefaultGroupCallAvatar +import com.nextcloud.talk.extensions.loadFederatedUserAvatar +import com.nextcloud.talk.extensions.loadFirstLetterAvatar +import com.nextcloud.talk.extensions.loadPhoneAvatar +import com.nextcloud.talk.extensions.loadTeamAvatar +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.models.json.participants.Participant.InCallFlags +import com.nextcloud.talk.models.json.status.StatusType +import com.nextcloud.talk.ui.StatusDrawable +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.DisplayUtils.convertDpToPixel +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFilterable +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import java.util.regex.Pattern + +class ParticipantItem( + private val context: Context, + val model: Participant, + private val user: User, + private val viewThemeUtils: ViewThemeUtils, + private val conversation: ConversationModel +) : AbstractFlexibleItem(), + IFilterable { + var isOnline = true + override fun equals(o: Any?): Boolean = + if (o is ParticipantItem) { + model.calculatedActorType == o.model.calculatedActorType && + model.calculatedActorId == o.model.calculatedActorId + } else { + false + } + + override fun hashCode(): Int = model.hashCode() + + override fun getLayoutRes(): Int = R.layout.rv_item_conversation_info_participant + + override fun createViewHolder( + view: View?, + adapter: FlexibleAdapter>? + ): ParticipantItemViewHolder = ParticipantItemViewHolder(view, adapter) + + @SuppressLint("SetTextI18n") + override fun bindViewHolder( + adapter: FlexibleAdapter>?, + holder: ParticipantItemViewHolder?, + position: Int, + payloads: List<*>? + ) { + drawStatus(holder!!) + setOnlineStateColor(holder) + holder.binding.nameText.text = model.displayName + + if (model.type == Participant.ParticipantType.GUEST && model.displayName.isNullOrBlank()) { + holder.binding.nameText.text = sharedApplication!!.getString(R.string.nc_guest) + } + + if (adapter!!.hasFilter()) { + viewThemeUtils.talk.themeAndHighlightText( + holder.binding.nameText, + model.displayName, + adapter.getFilter( + String::class.java + ).toString() + ) + } + loadAvatars(holder) + showCallIcons(holder) + setParticipantInfo(holder) + } + + @SuppressLint("SetTextI18n") + private fun setParticipantInfo(holder: ParticipantItemViewHolder) { + if (TextUtils.isEmpty(model.displayName) && + ( + model.type == Participant.ParticipantType.GUEST || + model.type == Participant.ParticipantType.USER_FOLLOWING_LINK + ) + ) { + holder.binding.nameText.text = sharedApplication!!.getString(R.string.nc_guest) + } + + var userType = "" + when (model.type) { + Participant.ParticipantType.OWNER, + Participant.ParticipantType.MODERATOR, + Participant.ParticipantType.GUEST_MODERATOR -> { + userType = sharedApplication!!.getString(R.string.nc_moderator) + } + + Participant.ParticipantType.USER -> { + userType = sharedApplication!!.getString(R.string.nc_user) + if (model.calculatedActorType == Participant.ActorType.GROUPS) { + userType = sharedApplication!!.getString(R.string.nc_group) + } + if (model.calculatedActorType == Participant.ActorType.CIRCLES) { + userType = sharedApplication!!.getString(R.string.nc_team) + } + } + + Participant.ParticipantType.GUEST -> { + userType = sharedApplication!!.getString(R.string.nc_guest) + if (model.calculatedActorType == Participant.ActorType.EMAILS) { + userType = sharedApplication!!.getString(R.string.nc_guest) + } + + if (model.invitedActorId?.isNotEmpty() == true && + ConversationUtils.isParticipantOwnerOrModerator(conversation) + ) { + holder.binding.conversationInfoStatusMessage.text = model.invitedActorId + alignUsernameVertical(holder, 0f) + } + } + + Participant.ParticipantType.USER_FOLLOWING_LINK -> { + userType = sharedApplication!!.getString(R.string.nc_following_link) + } + + else -> {} + } + if (userType != sharedApplication!!.getString(R.string.nc_user)) { + holder.binding.secondaryText.text = "($userType)" + } + } + + private fun setOnlineStateColor(holder: ParticipantItemViewHolder) { + if (!isOnline) { + holder.binding.nameText.setTextColor( + ResourcesCompat.getColor( + holder.binding.nameText.context.resources, + R.color.medium_emphasis_text, + null + ) + ) + holder.binding.avatarView.setAlpha(NOT_ONLINE_ALPHA) + } else { + holder.binding.nameText.setTextColor( + ResourcesCompat.getColor( + holder.binding.nameText.context.resources, + R.color.high_emphasis_text, + null + ) + ) + holder.binding.avatarView.setAlpha(1.0f) + } + } + + @SuppressLint("StringFormatInvalid") + private fun showCallIcons(holder: ParticipantItemViewHolder) { + val resources = sharedApplication!!.resources + val inCallFlag = model.inCall + if (inCallFlag and InCallFlags.WITH_PHONE.toLong() > 0) { + holder.binding.videoCallIcon.setImageResource(R.drawable.ic_call_grey_600_24dp) + holder.binding.videoCallIcon.setVisibility(View.VISIBLE) + holder.binding.videoCallIcon.setContentDescription( + resources.getString(R.string.nc_call_state_with_phone, model.displayName) + ) + } else if (inCallFlag and InCallFlags.WITH_VIDEO.toLong() > 0) { + holder.binding.videoCallIcon.setImageResource(R.drawable.ic_videocam_grey_600_24dp) + holder.binding.videoCallIcon.setVisibility(View.VISIBLE) + holder.binding.videoCallIcon.setContentDescription( + resources.getString(R.string.nc_call_state_with_video, model.displayName) + ) + } else if (inCallFlag > InCallFlags.DISCONNECTED) { + holder.binding.videoCallIcon.setImageResource(R.drawable.ic_mic_grey_600_24dp) + holder.binding.videoCallIcon.setVisibility(View.VISIBLE) + holder.binding.videoCallIcon.setContentDescription( + resources.getString(R.string.nc_call_state_in_call, model.displayName) + ) + } else { + holder.binding.videoCallIcon.setVisibility(View.GONE) + } + } + + private fun loadAvatars(holder: ParticipantItemViewHolder) { + when (model.calculatedActorType) { + Participant.ActorType.GROUPS -> { + holder.binding.avatarView.loadDefaultGroupCallAvatar(viewThemeUtils) + } + + Participant.ActorType.CIRCLES -> { + holder.binding.avatarView.loadTeamAvatar(viewThemeUtils) + } + + Participant.ActorType.USERS -> { + holder.binding.avatarView.loadUserAvatar(user, model.calculatedActorId!!, true, false) + } + + Participant.ActorType.GUESTS, Participant.ActorType.EMAILS -> { + val actorName = model.displayName + if (!actorName.isNullOrBlank()) { + holder.binding.avatarView.loadFirstLetterAvatar(actorName) + } else { + holder.binding.avatarView.loadDefaultAvatar(viewThemeUtils) + } + } + + Participant.ActorType.FEDERATED -> { + val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0 + holder.binding.avatarView.loadFederatedUserAvatar( + user, + user.baseUrl!!, + conversation.token, + model.actorId!!, + darkTheme, + true, + false + ) + } + + Participant.ActorType.PHONES -> { + holder.binding.avatarView.loadPhoneAvatar(viewThemeUtils) + } + + else -> { + Log.w(TAG, "Avatar not shown because of unknown ActorType " + model.calculatedActorType) + } + } + } + + @Suppress("MagicNumber") + private fun drawStatus(holder: ParticipantItemViewHolder) { + val size = convertDpToPixel(STATUS_SIZE_IN_DP, context) + holder.binding.userStatusImage.setImageDrawable( + StatusDrawable( + model.status, + NO_ICON, + size, + context.resources.getColor(R.color.bg_default), + context + ) + ) + if (model.statusMessage != null) { + holder.binding.conversationInfoStatusMessage.text = model.statusMessage + alignUsernameVertical(holder, 0f) + } else { + holder.binding.conversationInfoStatusMessage.text = "" + alignUsernameVertical(holder, 10f) + } + if (model.statusIcon != null && model.statusIcon!!.isNotEmpty()) { + holder.binding.participantStatusEmoji.setText(model.statusIcon) + } else { + holder.binding.participantStatusEmoji.visibility = View.GONE + } + if (model.status != null && model.status == StatusType.DND.string) { + if (model.statusMessage == null || model.statusMessage!!.isEmpty()) { + holder.binding.conversationInfoStatusMessage.setText(R.string.dnd) + } + } else if (model.status != null && model.status == StatusType.BUSY.string) { + if (model.statusMessage == null || model.statusMessage!!.isEmpty()) { + holder.binding.conversationInfoStatusMessage.setText(R.string.busy) + } + } else if (model.status != null && model.status == StatusType.AWAY.string) { + if (model.statusMessage == null || model.statusMessage!!.isEmpty()) { + holder.binding.conversationInfoStatusMessage.setText(R.string.away) + } + } + } + + private fun alignUsernameVertical(holder: ParticipantItemViewHolder, densityPixelsFromTop: Float) { + val layoutParams = holder.binding.nameText.layoutParams as ConstraintLayout.LayoutParams + layoutParams.topMargin = convertDpToPixel(densityPixelsFromTop, context).toInt() + holder.binding.nameText.setLayoutParams(layoutParams) + } + + override fun filter(constraint: String?): Boolean = + model.displayName != null && + ( + Pattern.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(model.displayName!!.trim()).find() || + Pattern.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(model.calculatedActorId!!.trim()).find() + ) + + class ParticipantItemViewHolder internal constructor(view: View?, adapter: FlexibleAdapter<*>?) : + FlexibleViewHolder(view, adapter) { + var binding: RvItemConversationInfoParticipantBinding + + init { + binding = RvItemConversationInfoParticipantBinding.bind(view!!) + } + } + + companion object { + private val TAG = ParticipantItem::class.simpleName + private const val STATUS_SIZE_IN_DP = 9f + private const val NO_ICON = "" + private const val NOT_ONLINE_ALPHA = 0.38f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/SpacerItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/SpacerItem.kt new file mode 100644 index 0000000..89ed9c2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/SpacerItem.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.items + +import android.view.View +import com.nextcloud.talk.R +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder + +class SpacerItem(private val height: Int) : AbstractFlexibleItem() { + + override fun getLayoutRes(): Int = R.layout.item_spacer + + override fun createViewHolder(view: View?, adapter: FlexibleAdapter?>?): ViewHolder = + ViewHolder(view!!, adapter!!) + + override fun bindViewHolder( + adapter: FlexibleAdapter?>?, + holder: ViewHolder, + position: Int, + payloads: MutableList? + ) { + holder.itemView.layoutParams.height = height + } + + override fun equals(other: Any?) = other is SpacerItem + + override fun hashCode(): Int = 0 + + class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/AdjustableMessageHolderInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/AdjustableMessageHolderInterface.kt new file mode 100644 index 0000000..8ce5187 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/AdjustableMessageHolderInterface.kt @@ -0,0 +1,51 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.adapters.messages + +import android.widget.RelativeLayout +import androidx.viewbinding.ViewBinding +import com.nextcloud.talk.databinding.ItemCustomOutcomingDeckCardMessageBinding +import com.nextcloud.talk.databinding.ItemCustomOutcomingLinkPreviewMessageBinding +import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding +import com.nextcloud.talk.databinding.ItemCustomOutcomingPollMessageBinding +import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding +import com.nextcloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.ConversationEnums.ConversationType + +interface AdjustableMessageHolderInterface { + + val binding: ViewBinding + + fun adjustIfNoteToSelf(currentConversation: ConversationModel?) { + if (currentConversation?.type == ConversationType.NOTE_TO_SELF) { + when (this.binding.javaClass) { + ItemCustomOutcomingTextMessageBinding::class.java -> + (this.binding as ItemCustomOutcomingTextMessageBinding).bubble + ItemCustomOutcomingDeckCardMessageBinding::class.java -> + (this.binding as ItemCustomOutcomingDeckCardMessageBinding).bubble + ItemCustomOutcomingLinkPreviewMessageBinding::class.java -> + (this.binding as ItemCustomOutcomingLinkPreviewMessageBinding).bubble + ItemCustomOutcomingPollMessageBinding::class.java -> + (this.binding as ItemCustomOutcomingPollMessageBinding).bubble + ItemCustomOutcomingVoiceMessageBinding::class.java -> + (this.binding as ItemCustomOutcomingVoiceMessageBinding).bubble + ItemCustomOutcomingLocationMessageBinding::class.java -> + (this.binding as ItemCustomOutcomingLocationMessageBinding).bubble + else -> null + }?.let { + RelativeLayout.LayoutParams(binding.root.layoutParams).apply { + marginStart = 0 + marginEnd = 0 + }.run { + it.layoutParams = this + } + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedMessageInterface.kt new file mode 100644 index 0000000..3c7d469 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/CallStartedMessageInterface.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +interface CallStartedMessageInterface { + fun joinAudioCall() + fun joinVideoCall() +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt new file mode 100644 index 0000000..c419f7b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/CommonMessageInterface.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import com.nextcloud.talk.chat.data.model.ChatMessage + +interface CommonMessageInterface { + fun onLongClickReactions(chatMessage: ChatMessage) + fun onClickReaction(chatMessage: ChatMessage, emoji: String) + fun onOpenMessageActionsDialog(chatMessage: ChatMessage) + fun openThread(chatMessage: ChatMessage) +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt new file mode 100644 index 0000000..5bef49b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingDeckCardViewHolder.kt @@ -0,0 +1,260 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.util.Log +import android.view.View +import android.widget.ImageView +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import autodagger.AutoInjector +import coil.load +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemCustomIncomingDeckCardMessageBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ChatMessageUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class IncomingDeckCardViewHolder(incomingView: View, payload: Any) : + MessageHolders + .IncomingTextMessageViewHolder(incomingView, payload) { + + private val binding: ItemCustomIncomingDeckCardMessageBinding = + ItemCustomIncomingDeckCardMessageBinding.bind(itemView) + + @Inject + lateinit var context: Context + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var ncApi: NcApi + + lateinit var message: ChatMessage + + lateinit var commonMessageInterface: CommonMessageInterface + + var stackName: String? = null + var cardName: String? = null + var boardName: String? = null + var cardLink: String? = null + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + this.message = message + sharedApplication!!.componentApplication.inject(this) + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + setAvatarAndAuthorOnMessageItem(message) + showDeckCard(message) + + colorizeMessageBubble(message) + + binding.cardView.findViewById(R.id.deckCardImage)?.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.SECONDARY) + } + + itemView.isSelected = false + + // parent message handling + setParentMessageDataOnMessageItem(message) + + binding.cardView.setOnLongClickListener { l: View? -> + commonMessageInterface.onOpenMessageActionsDialog(message) + true + } + + binding.cardView.setOnClickListener { + val browserIntent = Intent(Intent.ACTION_VIEW, cardLink!!.toUri()) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(browserIntent) + } + + itemView.setTag(R.string.replyable_message_view_tag, message.replyable) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageTime.context, + false, + viewThemeUtils + ) + } + + @SuppressLint("StringFormatInvalid") + private fun showDeckCard(message: ChatMessage) { + if (message.messageParameters != null && message.messageParameters!!.size > 0) { + for (key in message.messageParameters!!.keys) { + val individualHashMap: Map = message.messageParameters!![key]!! + if (individualHashMap["type"] == "deck-card") { + cardName = individualHashMap["name"] + stackName = individualHashMap["stackname"] + boardName = individualHashMap["boardname"] + cardLink = individualHashMap["link"] + } + } + } + + if (cardName?.isNotEmpty() == true) { + val cardDescription = String.format( + context.resources.getString(R.string.deck_card_description), + stackName, + boardName + ) + binding.cardName.visibility = View.VISIBLE + binding.cardDescription.visibility = View.VISIBLE + binding.cardName.text = cardName + binding.cardDescription.text = cardDescription + } + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) { + val actorName = message.actorDisplayName + if (!actorName.isNullOrBlank()) { + binding.messageAuthor.visibility = View.VISIBLE + binding.messageAuthor.text = actorName + binding.messageUserAvatar.setOnClickListener { + (payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context) + } + } else { + binding.messageAuthor.setText(R.string.nc_nick_guest) + } + + if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) { + ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils) + } else { + if (message.isOneToOneConversation || message.isFormerOneToOneConversation) { + binding.messageUserAvatar.visibility = View.GONE + } else { + binding.messageUserAvatar.visibility = View.INVISIBLE + } + binding.messageAuthor.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted) + } + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod") + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + binding.messageQuote.quotedMessageAuthor + .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast)) + + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + View.VISIBLE + } else { + View.GONE + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + companion object { + private val TAG = IncomingDeckCardViewHolder::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt new file mode 100644 index 0000000..250b34e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLinkPreviewMessageViewHolder.kt @@ -0,0 +1,252 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import android.view.View +import androidx.core.content.ContextCompat +import autodagger.AutoInjector +import coil.load +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemCustomIncomingLinkPreviewMessageBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ChatMessageUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class IncomingLinkPreviewMessageViewHolder(incomingView: View, payload: Any) : + MessageHolders.IncomingTextMessageViewHolder(incomingView, payload) { + + private val binding: ItemCustomIncomingLinkPreviewMessageBinding = + ItemCustomIncomingLinkPreviewMessageBinding.bind(itemView) + + @Inject + lateinit var context: Context + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var ncApi: NcApi + + lateinit var message: ChatMessage + + lateinit var commonMessageInterface: CommonMessageInterface + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + this.message = message + sharedApplication!!.componentApplication.inject(this) + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + var processedMessageText = messageUtils.enrichChatMessageText( + binding.messageText.context, + message, + true, + viewThemeUtils + ) + + processedMessageText = messageUtils.processMessageParameters( + binding.messageText.context, + viewThemeUtils, + processedMessageText!!, + message, + itemView + ) + + binding.messageText.text = processedMessageText + + setAvatarAndAuthorOnMessageItem(message) + + colorizeMessageBubble(message) + + itemView.isSelected = false + + // parent message handling + setParentMessageDataOnMessageItem(message) + + LinkPreview().showLink( + message, + ncApi, + binding.referenceInclude, + itemView.context + ) + binding.referenceInclude.referenceWrapper.setOnLongClickListener { l: View? -> + commonMessageInterface.onOpenMessageActionsDialog(message) + true + } + + itemView.setTag(R.string.replyable_message_view_tag, message.replyable) + + val chatActivity = commonMessageInterface as ChatActivity + val showThreadButton = chatActivity.conversationThreadId == null && message.isThread + if (showThreadButton) { + binding.reactions.threadButton.visibility = View.VISIBLE + binding.reactions.threadButton.setContent { + ThreadButtonComposable( + onButtonClick = { openThread(message) } + ) + } + } else { + binding.reactions.threadButton.visibility = View.GONE + } + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageTime.context, + false, + viewThemeUtils + ) + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun openThread(chatMessage: ChatMessage) { + commonMessageInterface.openThread(chatMessage) + } + + private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) { + val actorName = message.actorDisplayName + if (!actorName.isNullOrBlank()) { + binding.messageAuthor.visibility = View.VISIBLE + binding.messageAuthor.text = actorName + binding.messageUserAvatar.setOnClickListener { + (payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context) + } + } else { + binding.messageAuthor.setText(R.string.nc_nick_guest) + } + + if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) { + ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils) + } else { + if (message.isOneToOneConversation || message.isFormerOneToOneConversation) { + binding.messageUserAvatar.visibility = View.GONE + } else { + binding.messageUserAvatar.visibility = View.INVISIBLE + } + binding.messageAuthor.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted) + } + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod") + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + binding.messageQuote.quotedMessageAuthor + .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast)) + + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + View.VISIBLE + } else { + View.GONE + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + companion object { + private val TAG = IncomingLinkPreviewMessageViewHolder::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt new file mode 100644 index 0000000..f03c095 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt @@ -0,0 +1,286 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.util.Log +import android.util.TypedValue +import android.view.MotionEvent +import android.view.View +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.core.net.toUri +import autodagger.AutoInjector +import coil.load +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ChatMessageUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.UriUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.URLEncoder +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : + MessageHolders.IncomingTextMessageViewHolder(incomingView, payload) { + private val binding: ItemCustomIncomingLocationMessageBinding = + ItemCustomIncomingLocationMessageBinding.bind(itemView) + + var locationLon: String? = "" + var locationLat: String? = "" + var locationName: String? = "" + var locationGeoLink: String? = "" + + @Inject + lateinit var context: Context + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var dateUtils: DateUtils + + lateinit var commonMessageInterface: CommonMessageInterface + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + sharedApplication!!.componentApplication.inject(this) + + setAvatarAndAuthorOnMessageItem(message) + + colorizeMessageBubble(message) + + itemView.isSelected = false + + val textSize = context.resources!!.getDimension(R.dimen.chat_text_size) + binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + binding.messageText.text = message.text + + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + // parent message handling + setParentMessageDataOnMessageItem(message) + + // geo-location + setLocationDataOnMessageItem(message) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageText.context, + false, + viewThemeUtils + ) + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) { + val actorName = message.actorDisplayName + if (!actorName.isNullOrBlank()) { + binding.messageAuthor.visibility = View.VISIBLE + binding.messageAuthor.text = actorName + binding.messageUserAvatar.setOnClickListener { + (payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context) + } + } else { + binding.messageAuthor.setText(R.string.nc_nick_guest) + } + + if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) { + ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils) + } else { + if (message.isOneToOneConversation || message.isFormerOneToOneConversation) { + binding.messageUserAvatar.visibility = View.GONE + } else { + binding.messageUserAvatar.visibility = View.INVISIBLE + } + binding.messageAuthor.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted) + } + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod") + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + binding.messageQuote.quotedMessageAuthor + .setTextColor(context.resources.getColor(R.color.textColorMaxContrast, null)) + + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + View.VISIBLE + } else { + View.GONE + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + @SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility") + private fun setLocationDataOnMessageItem(message: ChatMessage) { + if (message.messageParameters != null && message.messageParameters!!.size > 0) { + for (key in message.messageParameters!!.keys) { + val individualHashMap: Map = message.messageParameters!![key]!! + if (individualHashMap["type"] == "geo-location") { + locationLon = individualHashMap["longitude"] + locationLat = individualHashMap["latitude"] + locationName = individualHashMap["name"] + locationGeoLink = individualHashMap["id"] + } + } + } + + binding.webview.settings.javaScriptEnabled = true + + binding.webview.webViewClient = object : WebViewClient() { + @Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)") + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean = + if (url != null && UriUtils.hasHttpProtocolPrefixed(url)) { + view?.context?.startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) + true + } else { + false + } + } + + val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html") + urlStringBuffer.append( + "?mapProviderUrl=" + URLEncoder.encode(context.getString(R.string.osm_tile_server_url)) + ) + urlStringBuffer.append( + "&mapProviderAttribution=" + URLEncoder.encode(context.getString(R.string.osm_tile_server_attributation)) + ) + urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat)) + urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon)) + urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName)) + urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink)) + + binding.webview.loadUrl(urlStringBuffer.toString()) + + binding.webview.setOnTouchListener(object : View.OnTouchListener { + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + when (event?.action) { + MotionEvent.ACTION_UP -> openGeoLink() + } + + return v?.onTouchEvent(event) ?: true + } + }) + } + + private fun openGeoLink() { + if (!locationGeoLink.isNullOrEmpty()) { + val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!) + val browserIntent = Intent(Intent.ACTION_VIEW, geoLinkWithMarker.toUri()) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(browserIntent) + } else { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Log.e(TAG, "locationGeoLink was null or empty") + } + } + + private fun addMarkerToGeoLink(locationGeoLink: String): String = locationGeoLink.replace("geo:", "geo:0,0?q=") + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + companion object { + private const val TAG = "LocInMessageView" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt new file mode 100644 index 0000000..83a06e3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt @@ -0,0 +1,254 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import android.view.View +import androidx.core.content.ContextCompat +import autodagger.AutoInjector +import coil.load +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemCustomIncomingPollMessageBinding +import com.nextcloud.talk.polls.ui.PollMainDialogFragment +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ChatMessageUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : + MessageHolders.IncomingTextMessageViewHolder(incomingView, payload) { + + private val binding: ItemCustomIncomingPollMessageBinding = ItemCustomIncomingPollMessageBinding.bind(itemView) + + @Inject + lateinit var context: Context + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var ncApi: NcApi + + lateinit var message: ChatMessage + + lateinit var commonMessageInterface: CommonMessageInterface + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + this.message = message + sharedApplication!!.componentApplication.inject(this) + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + setAvatarAndAuthorOnMessageItem(message) + + colorizeMessageBubble(message) + + itemView.isSelected = false + + // parent message handling + setParentMessageDataOnMessageItem(message) + + setPollPreview(message) + + val chatActivity = commonMessageInterface as ChatActivity + Thread().showThreadPreview( + chatActivity, + message, + threadBinding = binding.threadTitleWrapper, + reactionsBinding = binding.reactions, + openThread = { openThread(message) } + ) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageTime.context, + false, + viewThemeUtils + ) + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun openThread(chatMessage: ChatMessage) { + commonMessageInterface.openThread(chatMessage) + } + + private fun setPollPreview(message: ChatMessage) { + var pollId: String? = null + var pollName: String? = null + + if (message.messageParameters != null && message.messageParameters!!.size > 0) { + for (key in message.messageParameters!!.keys) { + val individualHashMap: Map = message.messageParameters!![key]!! + if (individualHashMap["type"] == "talk-poll") { + pollId = individualHashMap["id"] + pollName = individualHashMap["name"].toString() + } + } + } + + if (pollId != null && pollName != null) { + binding.messagePollTitle.text = pollName + + val roomToken = (payload as? MessagePayload)!!.roomToken + val isOwnerOrModerator = (payload as? MessagePayload)!!.isOwnerOrModerator ?: false + + binding.bubble.setOnClickListener { + val pollVoteDialog = PollMainDialogFragment.newInstance( + message.activeUser!!, + roomToken, + isOwnerOrModerator, + pollId, + pollName + ) + pollVoteDialog.show( + (binding.messagePollIcon.context as ChatActivity).supportFragmentManager, + TAG + ) + } + } + } + + private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) { + val actorName = message.actorDisplayName + if (!actorName.isNullOrBlank()) { + binding.messageAuthor.visibility = View.VISIBLE + binding.messageAuthor.text = actorName + binding.messageUserAvatar.setOnClickListener { + (payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context) + } + } else { + binding.messageAuthor.setText(R.string.nc_nick_guest) + } + + if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) { + ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils) + } else { + if (message.isOneToOneConversation || message.isFormerOneToOneConversation) { + binding.messageUserAvatar.visibility = View.GONE + } else { + binding.messageUserAvatar.visibility = View.INVISIBLE + } + binding.messageAuthor.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted) + } + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod") + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + binding.messageQuote.quotedMessageAuthor + .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast)) + + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + View.VISIBLE + } else { + View.GONE + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + companion object { + private val TAG = IncomingPollMessageViewHolder::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java new file mode 100644 index 0000000..7c13d01 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java @@ -0,0 +1,145 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022-2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2021-2022 Tim Krüger + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages; + +import android.text.Spanned; +import android.util.TypedValue; +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; + +import com.google.android.material.card.MaterialCardView; +import com.nextcloud.talk.R; +import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding; +import com.nextcloud.talk.databinding.ItemThreadTitleBinding; +import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding; +import com.nextcloud.talk.chat.data.model.ChatMessage; +import com.nextcloud.talk.utils.TextMatchers; + +import java.util.HashMap; +import java.util.Objects; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.emoji2.widget.EmojiTextView; + +public class IncomingPreviewMessageViewHolder extends PreviewMessageViewHolder { + private final ItemCustomIncomingPreviewMessageBinding binding; + + public IncomingPreviewMessageViewHolder(View itemView, Object payload) { + super(itemView, payload); + binding = ItemCustomIncomingPreviewMessageBinding.bind(itemView); + } + + @Override + public void onBind(@NonNull ChatMessage message) { + super.onBind(message); + if(!message.isVoiceMessage() + && !Objects.equals(message.getMessage(), "{file}") + ) { + Spanned processedMessageText = null; + binding.incomingPreviewMessageBubble.setBackgroundResource(R.drawable.shape_grouped_incoming_message); + if (viewThemeUtils != null ) { + processedMessageText = messageUtils.enrichChatMessageText( + binding.messageCaption.getContext(), + message, + true, + viewThemeUtils); + viewThemeUtils.talk.themeIncomingMessageBubble(binding.incomingPreviewMessageBubble, true, false, + false); + } + + if (processedMessageText != null) { + processedMessageText = messageUtils.processMessageParameters( + binding.messageCaption.getContext(), + viewThemeUtils, + processedMessageText, + message, + binding.incomingPreviewMessageBubble); + } + binding.incomingPreviewMessageBubble.setOnClickListener(null); + + float textSize = 0; + if (context != null) { + textSize = context.getResources().getDimension(R.dimen.chat_text_size); + } + HashMap> messageParameters = message.getMessageParameters(); + if ( + (messageParameters == null || messageParameters.size() <= 0) && + TextMatchers.isMessageWithSingleEmoticonOnly(message.getText()) + ) { + textSize = (float) (textSize * IncomingTextMessageViewHolder.TEXT_SIZE_MULTIPLIER); + itemView.setSelected(true); + } + binding.messageCaption.setVisibility(View.VISIBLE); + binding.messageCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + binding.messageCaption.setText(processedMessageText); + } else { + binding.incomingPreviewMessageBubble.setBackground(null); + binding.messageCaption.setVisibility(View.GONE); + } + binding.messageAuthor.setText(message.getActorDisplayName()); + binding.messageText.setTextColor(ContextCompat.getColor(binding.messageText.getContext(), + R.color.no_emphasis_text)); + binding.messageTime.setTextColor(ContextCompat.getColor(binding.messageText.getContext(), + R.color.no_emphasis_text)); + } + + @NonNull + @Override + public EmojiTextView getMessageText() { + return binding.messageText; + } + + @NonNull + @Override + public EmojiTextView getMessageCaption() { + return binding.messageCaption; + } + + @Override + public ProgressBar getProgressBar() { + return binding.progressBar; + } + + @NonNull + @Override + public View getPreviewContainer() { + return binding.previewContainer; + } + + @NonNull + @Override + public MaterialCardView getPreviewContactContainer() { + return binding.contactContainer; + } + + @NonNull + @Override + public ImageView getPreviewContactPhoto() { + return binding.contactPhoto; + } + + @NonNull + @Override + public EmojiTextView getPreviewContactName() { + return binding.contactName; + } + + @Override + public ProgressBar getPreviewContactProgressBar() { + return binding.contactProgressBar; + } + + @Override + public ReactionsInsideMessageBinding getReactionsBinding(){ return binding.reactions; } + + @Override + public ItemThreadTitleBinding getThreadsBinding(){ return binding.threadTitleWrapper; } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt new file mode 100644 index 0000000..ae3f691 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingTextMessageViewHolder.kt @@ -0,0 +1,428 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.content.Context +import android.util.Log +import android.util.TypedValue +import android.view.View +import android.widget.CheckBox +import androidx.core.content.ContextCompat +import androidx.core.text.toSpanned +import autodagger.AutoInjector +import coil.load +import com.google.android.flexbox.FlexboxLayout +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.ChatMessageRepository +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.ChatMessageUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.TextMatchers +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.Date +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class IncomingTextMessageViewHolder(itemView: View, payload: Any) : + MessageHolders.IncomingTextMessageViewHolder(itemView, payload) { + + private val binding: ItemCustomIncomingTextMessageBinding = ItemCustomIncomingTextMessageBinding.bind(itemView) + + @Inject + lateinit var context: Context + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + lateinit var commonMessageInterface: CommonMessageInterface + + @Inject + lateinit var chatRepository: ChatMessageRepository + + private var job: Job? = null + + override fun onBind(message: ChatMessage) { + super.onBind(message) + sharedApplication!!.componentApplication.inject(this) + setAvatarAndAuthorOnMessageItem(message) + colorizeMessageBubble(message) + itemView.isSelected = false + val user = currentUserProvider.currentUser.blockingGet() + val hasCheckboxes = processCheckboxes( + message, + user + ) + processMessage(message, hasCheckboxes) + } + + private fun processMessage(message: ChatMessage, hasCheckboxes: Boolean) { + var textSize = context.resources!!.getDimension(R.dimen.chat_text_size) + if (!hasCheckboxes) { + binding.messageText.visibility = View.VISIBLE + binding.checkboxContainer.visibility = View.GONE + var processedMessageText = messageUtils.enrichChatMessageText( + binding.messageText.context, + message, + true, + viewThemeUtils + ) + + val spansFromString: Array = processedMessageText!!.getSpans( + 0, + processedMessageText.length, + Any::class.java + ) + + if (spansFromString.isNotEmpty()) { + binding.bubble.layoutParams.apply { + width = FlexboxLayout.LayoutParams.MATCH_PARENT + } + binding.messageText.layoutParams.apply { + width = FlexboxLayout.LayoutParams.MATCH_PARENT + } + } else { + binding.bubble.layoutParams.apply { + width = FlexboxLayout.LayoutParams.WRAP_CONTENT + } + binding.messageText.layoutParams.apply { + width = FlexboxLayout.LayoutParams.WRAP_CONTENT + } + } + + processedMessageText = messageUtils.processMessageParameters( + binding.messageText.context, + viewThemeUtils, + processedMessageText, + message, + itemView + ) + val messageParameters = message.messageParameters + if ( + (messageParameters == null || messageParameters.size <= 0) && + TextMatchers.isMessageWithSingleEmoticonOnly(message.text) + ) { + textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat() + itemView.isSelected = true + binding.messageAuthor.visibility = View.GONE + } + binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + binding.messageText.text = processedMessageText + // just for debugging: + // binding.messageText.text = + // SpannableStringBuilder(processedMessageText).append(" (" + message.jsonMessageId + ")") + } else { + binding.messageText.visibility = View.GONE + binding.checkboxContainer.visibility = View.VISIBLE + } + + if (message.lastEditTimestamp != 0L && !message.isDeleted) { + binding.messageEditIndicator.visibility = View.VISIBLE + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!) + } else { + binding.messageEditIndicator.visibility = View.GONE + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + } + viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) + + // parent message handling + val chatActivity = commonMessageInterface as ChatActivity + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + processParentMessage(message) + View.VISIBLE + } else { + View.GONE + } + + binding.messageQuote.quotedChatMessageView.setOnLongClickListener { l: View? -> + commonMessageInterface.onOpenMessageActionsDialog(message) + true + } + + itemView.setTag(R.string.replyable_message_view_tag, message.replyable) + + Thread().showThreadPreview( + chatActivity, + message, + threadBinding = binding.threadTitleWrapper, + reactionsBinding = binding.reactions, + openThread = { openThread(message) } + ) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageText.context, + false, + viewThemeUtils + ) + } + + private fun processCheckboxes(chatMessage: ChatMessage, user: User): Boolean { + val chatActivity = commonMessageInterface as ChatActivity + val message = chatMessage.message!!.toSpanned() + val messageTextView = binding.messageText + val checkBoxContainer = binding.checkboxContainer + val isOlderThanTwentyFourHours = chatMessage + .createdAt + .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE)) + + val messageIsEditable = hasSpreedFeatureCapability( + user.capabilities?.spreedCapability!!, + SpreedFeatures.EDIT_MESSAGES + ) && + !isOlderThanTwentyFourHours + + checkBoxContainer.removeAllViews() + val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE) + val matches = regex.findAll(message) + + if (matches.none()) return false + + val firstPart = message.toString().substringBefore("\n- [") + messageTextView.text = messageUtils.enrichChatMessageText( + binding.messageText.context, + firstPart, + true, + viewThemeUtils + ) + + val checkboxList = mutableListOf() + + matches.forEach { matchResult -> + val isChecked = matchResult.groupValues[CHECKED_GROUP_INDEX] == "X" || + matchResult.groupValues[CHECKED_GROUP_INDEX] == "x" + val taskText = matchResult.groupValues[TASK_TEXT_GROUP_INDEX].trim() + + val checkBox = CheckBox(checkBoxContainer.context).apply { + text = taskText + this.isChecked = isChecked + this.isEnabled = ( + chatMessage.actorType == "bots" || + chatActivity.userAllowedByPrivilages(chatMessage) + ) && + messageIsEditable + + setTextColor(ContextCompat.getColor(context, R.color.no_emphasis_text)) + + setOnCheckedChangeListener { _, _ -> + updateCheckboxStates(chatMessage, user, checkboxList) + } + } + checkBoxContainer.addView(checkBox) + checkboxList.add(checkBox) + viewThemeUtils.platform.themeCheckbox(checkBox) + } + return true + } + + private fun updateCheckboxStates(chatMessage: ChatMessage, user: User, checkboxes: List) { + job = CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.IO) { + val apiVersion: Int = ApiUtils.getChatApiVersion( + user.capabilities?.spreedCapability!!, + intArrayOf(1) + ) + val updatedMessage = updateMessageWithCheckboxStates(chatMessage.message!!, checkboxes) + chatRepository.editChatMessage( + user.getCredentials(), + ApiUtils.getUrlForChatMessage(apiVersion, user.baseUrl!!, chatMessage.token!!, chatMessage.id), + updatedMessage + ).collect { result -> + withContext(Dispatchers.Main) { + if (result.isSuccess) { + val editedMessage = result.getOrNull()?.ocs?.data!!.parentMessage!! + Log.d(TAG, "EditedMessage: $editedMessage") + binding.messageEditIndicator.apply { + visibility = View.VISIBLE + } + binding.messageTime.text = + dateUtils.getLocalTimeStringFromTimestamp(editedMessage.lastEditTimestamp!!) + } else { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + } + } + } + } + } + + private fun updateMessageWithCheckboxStates(originalMessage: String, checkboxes: List): String { + var updatedMessage = originalMessage + val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE) + + checkboxes.forEach { _ -> + updatedMessage = regex.replace(updatedMessage) { matchResult -> + val taskText = matchResult.groupValues[TASK_TEXT_GROUP_INDEX].trim() + val checkboxState = if (checkboxes.find { it.text == taskText }?.isChecked == true) "X" else " " + "- [$checkboxState] $taskText" + } + } + return updatedMessage + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun openThread(chatMessage: ChatMessage) { + commonMessageInterface.openThread(chatMessage) + } + + private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) { + val actorName = message.actorDisplayName + if (!actorName.isNullOrBlank()) { + binding.messageAuthor.visibility = View.VISIBLE + binding.messageAuthor.text = actorName + binding.messageUserAvatar.setOnClickListener { + (payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context) + } + } else { + binding.messageAuthor.setText(R.string.nc_nick_guest) + } + + if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) { + ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils) + } else { + if (message.isOneToOneConversation || message.isFormerOneToOneConversation) { + binding.messageUserAvatar.visibility = View.GONE + } else { + binding.messageUserAvatar.visibility = View.INVISIBLE + } + binding.messageAuthor.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeIncomingMessageBubble(bubble, message.isGrouped, message.isDeleted) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun processParentMessage(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = + if (parentChatMessage.actorDisplayName.isNullOrEmpty()) { + context.getText(R.string.nc_nick_guest) + } else { + parentChatMessage.actorDisplayName + } + + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView, + R.color.high_emphasis_text + ) + + binding.messageQuote.quotedChatMessageView.setOnClickListener { + chatActivity.jumpToQuotedMessage(parentChatMessage) + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + override fun viewDetached() { + super.viewDetached() + job?.cancel() + } + + companion object { + const val TEXT_SIZE_MULTIPLIER = 2.5 + private val TAG = IncomingTextMessageViewHolder::class.java.simpleName + private const val CHECKED_GROUP_INDEX = 2 + private const val TASK_TEXT_GROUP_INDEX = 3 + private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt new file mode 100644 index 0000000..97d01f6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt @@ -0,0 +1,390 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import android.view.View +import android.widget.SeekBar +import androidx.core.content.ContextCompat +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import coil.load +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ChatMessageUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ExecutionException +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : + MessageHolders.IncomingTextMessageViewHolder(incomingView, payload) { + + private val binding: ItemCustomIncomingVoiceMessageBinding = ItemCustomIncomingVoiceMessageBinding.bind(itemView) + + @JvmField + @Inject + var context: Context? = null + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var appPreferences: AppPreferences + + lateinit var message: ChatMessage + + lateinit var voiceMessageInterface: VoiceMessageInterface + lateinit var commonMessageInterface: CommonMessageInterface + private var isBound = false + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + if (isBound) { + handleIsPlayingVoiceMessageState(message) + return + } + + this.message = message + sharedApplication!!.componentApplication.inject(this) + + val filename = message.selectedIndividualHashMap!!["name"] + val retrieved = appPreferences.getWaveFormFromFile(filename) + if (retrieved.isNotEmpty() && + message.voiceMessageFloatArray == null || + message.voiceMessageFloatArray?.isEmpty() == true + ) { + message.voiceMessageFloatArray = retrieved.toFloatArray() + binding.seekbar.setWaveData(message.voiceMessageFloatArray!!) + } + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + setAvatarAndAuthorOnMessageItem(message) + + colorizeMessageBubble(message) + + itemView.isSelected = false + + // parent message handling + setParentMessageDataOnMessageItem(message) + + updateDownloadState(message) + binding.seekbar.max = MAX + viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar) + viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT) + + showVoiceMessageDuration(message) + if (message.isDownloadingVoiceMessage) { + showVoiceMessageLoading() + } else { + if (message.voiceMessageFloatArray == null || message.voiceMessageFloatArray!!.isEmpty()) { + binding.seekbar.setWaveData(FloatArray(0)) + } else { + binding.seekbar.setWaveData(message.voiceMessageFloatArray!!) + } + binding.progressBar.visibility = View.GONE + } + + if (message.resetVoiceMessage) { + resetVoiceMessage(message) + } + + binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onStopTrackingTouch(seekBar: SeekBar) { + // unused atm + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + // unused atm + } + + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (fromUser) { + voiceMessageInterface.updateMediaPlayerProgressBySlider(message, progress) + } + } + }) + + CoroutineScope(Dispatchers.Default).launch { + (voiceMessageInterface as ChatActivity).chatViewModel.voiceMessagePlayBackUIFlow.onEach { speed -> + withContext(Dispatchers.Main) { + binding.playbackSpeedControlBtn.setSpeed(speed) + } + }.collect() + } + + binding.playbackSpeedControlBtn.setSpeed(appPreferences.getPreferredPlayback(message.actorId)) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageTime.context, + false, + viewThemeUtils + ) + + isBound = true + } + + private fun showVoiceMessageDuration(message: ChatMessage) { + if (message.voiceMessageDuration > 0) { + binding.voiceMessageDuration.visibility = View.VISIBLE + } else { + binding.voiceMessageDuration.visibility = View.INVISIBLE + } + } + + private fun resetVoiceMessage(chatMessage: ChatMessage) { + binding.playPauseBtn.visibility = View.VISIBLE + binding.playPauseBtn.icon = ContextCompat.getDrawable( + context!!, + R.drawable.ic_baseline_play_arrow_voice_message_24 + ) + binding.seekbar.progress = SEEKBAR_START + chatMessage.resetVoiceMessage = false + chatMessage.voiceMessagePlayedSeconds = 0 + showVoiceMessageDuration(message) + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun handleIsPlayingVoiceMessageState(message: ChatMessage) { + colorizeMessageBubble(message) + if (message.isPlayingVoiceMessage) { + showPlayButton() + binding.playPauseBtn.icon = ContextCompat.getDrawable( + context!!, + R.drawable.ic_baseline_pause_voice_message_24 + ) + + val d = message.voiceMessageDuration.toLong() + val t = message.voiceMessagePlayedSeconds.toLong() + binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t) + binding.voiceMessageDuration.visibility = View.VISIBLE + binding.seekbar.progress = message.voiceMessageSeekbarProgress + } else { + showVoiceMessageDuration(message) + binding.playPauseBtn.visibility = View.VISIBLE + binding.playPauseBtn.icon = ContextCompat.getDrawable( + context!!, + R.drawable.ic_baseline_play_arrow_voice_message_24 + ) + } + } + + private fun updateDownloadState(message: ChatMessage) { + // check if download worker is already running + val fileId = message.selectedIndividualHashMap!!["id"] + val workers = WorkManager.getInstance(context!!).getWorkInfosByTag(fileId!!) + + try { + for (workInfo in workers.get()) { + if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { + showVoiceMessageLoading() + WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id) + .observeForever { info: WorkInfo? -> + showStatus(info) + } + } + } + } catch (e: ExecutionException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } catch (e: InterruptedException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } + } + + private fun showStatus(info: WorkInfo?) { + if (info != null) { + when (info.state) { + WorkInfo.State.RUNNING -> { + Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder") + showVoiceMessageLoading() + } + + WorkInfo.State.SUCCEEDED -> { + Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder") + showPlayButton() + } + + WorkInfo.State.FAILED -> { + Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder") + showPlayButton() + } + + else -> { + } + } + } + } + + private fun showPlayButton() { + binding.playPauseBtn.visibility = View.VISIBLE + binding.progressBar.visibility = View.GONE + } + + private fun showVoiceMessageLoading() { + binding.playPauseBtn.visibility = View.GONE + binding.progressBar.visibility = View.VISIBLE + } + + private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) { + val actorName = message.actorDisplayName + if (!actorName.isNullOrBlank()) { + binding.messageAuthor.visibility = View.VISIBLE + binding.messageAuthor.text = actorName + binding.messageUserAvatar.setOnClickListener { + (payload as? MessagePayload)?.profileBottomSheet?.showFor(message, itemView.context) + } + } else { + binding.messageAuthor.setText(R.string.nc_nick_guest) + } + + if (!message.isGrouped && !message.isOneToOneConversation && !message.isFormerOneToOneConversation) { + ChatMessageUtils().setAvatarOnMessage(binding.messageUserAvatar, message, viewThemeUtils) + } else { + if (message.isOneToOneConversation || message.isFormerOneToOneConversation) { + binding.messageUserAvatar.visibility = View.GONE + } else { + binding.messageUserAvatar.visibility = View.INVISIBLE + } + binding.messageAuthor.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeIncomingMessageBubble( + bubble, + message.isGrouped, + message.isDeleted, + message.wasPlayedVoiceMessage + ) + } + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod") + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context!!.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + binding.messageQuote.quotedMessageAuthor + .setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast)) + + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + View.VISIBLE + } else { + View.GONE + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + fun assignVoiceMessageInterface(voiceMessageInterface: VoiceMessageInterface) { + this.voiceMessageInterface = voiceMessageInterface + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + companion object { + private const val TAG = "VoiceInMessageView" + private const val SEEKBAR_START: Int = 0 + private const val MAX: Int = 100 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt new file mode 100644 index 0000000..acfad29 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/LinkPreview.kt @@ -0,0 +1,119 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.content.Context +import android.content.Intent +import android.util.Log +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import coil.load +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ReferenceInsideMessageBinding +import com.nextcloud.talk.models.json.opengraph.OpenGraphOverall +import com.nextcloud.talk.utils.ApiUtils +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers + +class LinkPreview { + + fun showLink(message: ChatMessage, ncApi: NcApi, binding: ReferenceInsideMessageBinding, context: Context) { + binding.referenceName.text = "" + binding.referenceDescription.text = "" + binding.referenceLink.text = "" + binding.referenceThumbImage.setImageDrawable(null) + + if (!message.extractedUrlToPreview.isNullOrEmpty()) { + val credentials: String = ApiUtils.getCredentials(message.activeUser?.username, message.activeUser?.token)!! + val openGraphLink = ApiUtils.getUrlForOpenGraph(message.activeUser?.baseUrl!!) + ncApi.getOpenGraph( + credentials, + openGraphLink, + message.extractedUrlToPreview + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(openGraphOverall: OpenGraphOverall) { + val reference = openGraphOverall.ocs?.data?.references?.entries?.iterator()?.next()?.value + + if (reference != null) { + val referenceName = reference.openGraphObject?.name + if (!referenceName.isNullOrEmpty()) { + binding.referenceName.visibility = View.VISIBLE + binding.referenceName.text = referenceName + } else { + binding.referenceName.visibility = View.GONE + } + + val referenceDescription = reference.openGraphObject?.description + if (!referenceDescription.isNullOrEmpty()) { + binding.referenceDescription.visibility = View.VISIBLE + binding.referenceDescription.text = referenceDescription + } else { + binding.referenceDescription.visibility = View.GONE + } + + val referenceLink = reference.openGraphObject?.link + if (!referenceLink.isNullOrEmpty()) { + binding.referenceLink.visibility = View.VISIBLE + binding.referenceLink.text = referenceLink.replace(HTTPS_PROTOCOL, "") + } else { + binding.referenceLink.visibility = View.GONE + } + + val referenceThumbUrl = reference.openGraphObject?.thumb + var backgroundId = R.drawable.link_text_background + if (!referenceThumbUrl.isNullOrEmpty()) { + binding.referenceThumbImage.visibility = View.VISIBLE + binding.referenceThumbImage.load(referenceThumbUrl) + } else { + backgroundId = R.drawable.link_text_no_preview_background + binding.referenceThumbImage.visibility = View.GONE + } + binding.referenceMetadataContainer.background = ContextCompat.getDrawable( + binding.referenceMetadataContainer.context, + backgroundId + ) + + binding.referenceWrapper.setOnClickListener { + val browserIntent = Intent(Intent.ACTION_VIEW, referenceLink!!.toUri()) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(browserIntent) + } + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failed to get openGraph data", e) + binding.referenceName.visibility = View.GONE + binding.referenceDescription.visibility = View.GONE + binding.referenceLink.visibility = View.GONE + binding.referenceThumbImage.visibility = View.GONE + } + + override fun onComplete() { + // unused atm + } + }) + } + } + + companion object { + private val TAG = LinkPreview::class.java.simpleName + private const val HTTPS_PROTOCOL = "https://" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MessagePayload.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MessagePayload.kt new file mode 100644 index 0000000..9f6d125 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MessagePayload.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet + +data class MessagePayload( + var roomToken: String, + val isOwnerOrModerator: Boolean?, + val profileBottomSheet: ProfileBottomSheet +) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingDeckCardViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingDeckCardViewHolder.kt new file mode 100644 index 0000000..e4433f3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingDeckCardViewHolder.kt @@ -0,0 +1,252 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.util.Log +import android.view.View +import android.widget.ImageView +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import autodagger.AutoInjector +import coil.load +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemCustomOutcomingDeckCardMessageBinding +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class OutcomingDeckCardViewHolder(outcomingView: View) : + MessageHolders.OutcomingTextMessageViewHolder(outcomingView), + AdjustableMessageHolderInterface { + + override val binding: ItemCustomOutcomingDeckCardMessageBinding = ItemCustomOutcomingDeckCardMessageBinding.bind( + itemView + ) + + @Inject + lateinit var context: Context + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var ncApi: NcApi + + lateinit var message: ChatMessage + + @Inject + lateinit var dateUtils: DateUtils + + lateinit var commonMessageInterface: CommonMessageInterface + + var stackName: String? = null + var cardName: String? = null + var boardName: String? = null + var cardLink: String? = null + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + this.message = message + sharedApplication!!.componentApplication.inject(this) + viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + colorizeMessageBubble(message) + + binding.cardView.findViewById(R.id.deckCardImage)?.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.SECONDARY) + } + + itemView.isSelected = false + + showDeckCard(message) + + // parent message handling + setParentMessageDataOnMessageItem(message) + + val readStatusDrawableInt = when (message.readStatus) { + ReadStatus.READ -> R.drawable.ic_check_all + ReadStatus.SENT -> R.drawable.ic_check + else -> null + } + + val readStatusContentDescriptionString = when (message.readStatus) { + ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read) + ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent) + else -> null + } + + readStatusDrawableInt?.let { drawableInt -> + AppCompatResources.getDrawable(context, drawableInt)?.let { + binding.checkMark.setImageDrawable(it) + viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark) + } + } + + binding.checkMark.contentDescription = readStatusContentDescriptionString + + binding.cardView.setOnClickListener { + val browserIntent = Intent(Intent.ACTION_VIEW, cardLink!!.toUri()) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(browserIntent) + } + + itemView.setTag(R.string.replyable_message_view_tag, message.replyable) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageTime.context, + true, + viewThemeUtils + ) + } + + private fun showDeckCard(message: ChatMessage) { + if (message.messageParameters != null && message.messageParameters!!.size > 0) { + for (key in message.messageParameters!!.keys) { + val individualHashMap: Map = message.messageParameters!![key]!! + if (individualHashMap["type"] == "deck-card") { + cardName = individualHashMap["name"] + stackName = individualHashMap["stackname"] + boardName = individualHashMap["boardname"] + cardLink = individualHashMap["link"] + } + } + } + val cardDescription = String.format( + context.resources.getString(R.string.deck_card_description), + stackName, + boardName + ) + + binding.cardName.visibility = View.VISIBLE + binding.cardDescription.visibility = View.VISIBLE + binding.cardName.text = cardName + binding.cardDescription.text = cardDescription + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod") + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + true, + viewThemeUtils + ) + + binding.messageQuote.quotedMessageAuthor + .setTextColor(ContextCompat.getColor(context, R.color.textColorMaxContrast)) + + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + View.VISIBLE + } else { + View.GONE + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted) + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + companion object { + private val TAG = OutcomingDeckCardViewHolder::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt new file mode 100644 index 0000000..fcf78cf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLinkPreviewMessageViewHolder.kt @@ -0,0 +1,243 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import autodagger.AutoInjector +import coil.load +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemCustomOutcomingLinkPreviewMessageBinding +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class OutcomingLinkPreviewMessageViewHolder(outcomingView: View, payload: Any) : + MessageHolders.OutcomingTextMessageViewHolder(outcomingView, payload), + AdjustableMessageHolderInterface { + + override val binding: ItemCustomOutcomingLinkPreviewMessageBinding = + ItemCustomOutcomingLinkPreviewMessageBinding.bind(itemView) + + @Inject + lateinit var context: Context + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var ncApi: NcApi + + lateinit var message: ChatMessage + + lateinit var commonMessageInterface: CommonMessageInterface + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + this.message = message + sharedApplication!!.componentApplication.inject(this) + viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + colorizeMessageBubble(message) + var processedMessageText = + messageUtils.enrichChatMessageText(binding.messageText.context, message, false, viewThemeUtils) + processedMessageText = messageUtils.processMessageParameters( + binding.messageText.context, + viewThemeUtils, + processedMessageText!!, + message, + itemView + ) + + binding.messageText.text = processedMessageText + + itemView.isSelected = false + + // parent message handling + setParentMessageDataOnMessageItem(message) + + val readStatusDrawableInt = when (message.readStatus) { + ReadStatus.READ -> R.drawable.ic_check_all + ReadStatus.SENT -> R.drawable.ic_check + else -> null + } + + val readStatusContentDescriptionString = when (message.readStatus) { + ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read) + ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent) + else -> null + } + + readStatusDrawableInt?.let { drawableInt -> + AppCompatResources.getDrawable(context, drawableInt)?.let { + binding.checkMark.setImageDrawable(it) + viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark) + } + } + + binding.checkMark.contentDescription = readStatusContentDescriptionString + + LinkPreview().showLink( + message, + ncApi, + binding.referenceInclude, + itemView.context + ) + binding.referenceInclude.referenceWrapper.setOnLongClickListener { l: View? -> + commonMessageInterface.onOpenMessageActionsDialog(message) + true + } + + itemView.setTag(R.string.replyable_message_view_tag, message.replyable) + + val chatActivity = commonMessageInterface as ChatActivity + val showThreadButton = chatActivity.conversationThreadId == null && message.isThread + if (showThreadButton) { + binding.reactions.threadButton.visibility = View.VISIBLE + binding.reactions.threadButton.setContent { + ThreadButtonComposable( + onButtonClick = { openThread(message) } + ) + } + } else { + binding.reactions.threadButton.visibility = View.GONE + } + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageTime.context, + true, + viewThemeUtils + ) + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun openThread(chatMessage: ChatMessage) { + commonMessageInterface.openThread(chatMessage) + } + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod") + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + false, + viewThemeUtils + ) + viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) + viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + View.VISIBLE + } else { + View.GONE + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted) + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + companion object { + private val TAG = OutcomingLinkPreviewMessageViewHolder::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt new file mode 100644 index 0000000..6c91605 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt @@ -0,0 +1,292 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.util.Log +import android.util.TypedValue +import android.view.MotionEvent +import android.view.View +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.net.toUri +import autodagger.AutoInjector +import coil.load +import com.google.android.flexbox.FlexboxLayout +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.UriUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.URLEncoder +import javax.inject.Inject +import kotlin.math.roundToInt + +@AutoInjector(NextcloudTalkApplication::class) +class OutcomingLocationMessageViewHolder(incomingView: View) : + MessageHolders.OutcomingTextMessageViewHolder(incomingView), + AdjustableMessageHolderInterface { + + override val binding: ItemCustomOutcomingLocationMessageBinding = + ItemCustomOutcomingLocationMessageBinding.bind(itemView) + private val realView: View = itemView + + var locationLon: String? = "" + var locationLat: String? = "" + var locationName: String? = "" + var locationGeoLink: String? = "" + + @Inject + lateinit var context: Context + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var dateUtils: DateUtils + + lateinit var commonMessageInterface: CommonMessageInterface + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + sharedApplication!!.componentApplication.inject(this) + viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + realView.isSelected = false + val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams + layoutParams.isWrapBefore = false + + val textSize = context.resources.getDimension(R.dimen.chat_text_size) + + colorizeMessageBubble(message) + binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + binding.messageTime.layoutParams = layoutParams + + binding.messageText.text = message.text + + // parent message handling + setParentMessageDataOnMessageItem(message) + + val readStatusDrawableInt = when (message.readStatus) { + ReadStatus.READ -> R.drawable.ic_check_all + ReadStatus.SENT -> R.drawable.ic_check + else -> null + } + + val readStatusContentDescriptionString = when (message.readStatus) { + ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read) + ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent) + else -> null + } + + readStatusDrawableInt?.let { drawableInt -> + AppCompatResources.getDrawable(context, drawableInt)?.let { + binding.checkMark.setImageDrawable(it) + viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark) + } + } + + binding.checkMark.contentDescription = readStatusContentDescriptionString + + // geo-location + setLocationDataOnMessageItem(message) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageText.context, + true, + viewThemeUtils + ) + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + @SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility") + private fun setLocationDataOnMessageItem(message: ChatMessage) { + if (message.messageParameters != null && message.messageParameters!!.size > 0) { + for (key in message.messageParameters!!.keys) { + val individualHashMap: Map = message.messageParameters!![key]!! + if (individualHashMap["type"] == "geo-location") { + locationLon = individualHashMap["longitude"] + locationLat = individualHashMap["latitude"] + locationName = individualHashMap["name"] + locationGeoLink = individualHashMap["id"] + } + } + } + + binding.webview.settings.javaScriptEnabled = true + + binding.webview.webViewClient = object : WebViewClient() { + @Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)") + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean = + if (url != null && UriUtils.hasHttpProtocolPrefixed(url)) { + view?.context?.startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) + true + } else { + false + } + } + + val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html") + urlStringBuffer.append( + "?mapProviderUrl=" + URLEncoder.encode(context.getString(R.string.osm_tile_server_url)) + ) + urlStringBuffer.append( + "&mapProviderAttribution=" + URLEncoder.encode( + context.getString( + R.string + .osm_tile_server_attributation + ) + ) + ) + urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat)) + urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon)) + urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName)) + urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink)) + + binding.webview.loadUrl(urlStringBuffer.toString()) + + binding.webview.setOnTouchListener(object : View.OnTouchListener { + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + when (event?.action) { + MotionEvent.ACTION_UP -> openGeoLink() + } + + return v?.onTouchEvent(event) ?: true + } + }) + } + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod") + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + false, + viewThemeUtils + ) + viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) + viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + View.VISIBLE + } else { + View.GONE + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted) + } + + private fun openGeoLink() { + if (!locationGeoLink.isNullOrEmpty()) { + val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!) + val browserIntent = Intent(Intent.ACTION_VIEW, geoLinkWithMarker.toUri()) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(browserIntent) + } else { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Log.e(TAG, "locationGeoLink was null or empty") + } + } + + private fun addMarkerToGeoLink(locationGeoLink: String): String = locationGeoLink.replace("geo:", "geo:0,0?q=") + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + companion object { + private const val TAG = "LocOutMessageView" + private const val HALF_ALPHA_INT: Int = 255 / 2 + private val ALPHA_60_INT: Int = (255 * 0.6).roundToInt() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt new file mode 100644 index 0000000..ac952cd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt @@ -0,0 +1,252 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import autodagger.AutoInjector +import coil.load +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemCustomOutcomingPollMessageBinding +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.polls.ui.PollMainDialogFragment +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class OutcomingPollMessageViewHolder(outcomingView: View, payload: Any) : + MessageHolders.OutcomingTextMessageViewHolder(outcomingView, payload), + AdjustableMessageHolderInterface { + + override val binding: ItemCustomOutcomingPollMessageBinding = ItemCustomOutcomingPollMessageBinding.bind(itemView) + + @Inject + lateinit var context: Context + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var ncApi: NcApi + + lateinit var message: ChatMessage + + lateinit var commonMessageInterface: CommonMessageInterface + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + this.message = message + sharedApplication!!.componentApplication.inject(this) + viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + colorizeMessageBubble(message) + + itemView.isSelected = false + + // parent message handling + setParentMessageDataOnMessageItem(message) + + val readStatusDrawableInt = when (message.readStatus) { + ReadStatus.READ -> R.drawable.ic_check_all + ReadStatus.SENT -> R.drawable.ic_check + else -> null + } + + val readStatusContentDescriptionString = when (message.readStatus) { + ReadStatus.READ -> context.resources?.getString(R.string.nc_message_read) + ReadStatus.SENT -> context.resources?.getString(R.string.nc_message_sent) + else -> null + } + + readStatusDrawableInt?.let { drawableInt -> + AppCompatResources.getDrawable(context, drawableInt)?.let { + binding.checkMark.setImageDrawable(it) + viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark) + } + } + + binding.checkMark.contentDescription = readStatusContentDescriptionString + + setPollPreview(message) + + val chatActivity = commonMessageInterface as ChatActivity + Thread().showThreadPreview( + chatActivity, + message, + threadBinding = binding.threadTitleWrapper, + reactionsBinding = binding.reactions, + openThread = { openThread(message) } + ) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageTime.context, + true, + viewThemeUtils + ) + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun openThread(chatMessage: ChatMessage) { + commonMessageInterface.openThread(chatMessage) + } + + private fun setPollPreview(message: ChatMessage) { + var pollId: String? = null + var pollName: String? = null + + if (message.messageParameters != null && message.messageParameters!!.size > 0) { + for (key in message.messageParameters!!.keys) { + val individualHashMap: Map = message.messageParameters!![key]!! + if (individualHashMap["type"] == "talk-poll") { + pollId = individualHashMap["id"] + pollName = individualHashMap["name"].toString() + } + } + } + + if (pollId != null && pollName != null) { + binding.messagePollTitle.text = pollName + + val roomToken = (payload as? MessagePayload)!!.roomToken + val isOwnerOrModerator = (payload as? MessagePayload)!!.isOwnerOrModerator ?: false + + binding.bubble.setOnClickListener { + val pollVoteDialog = PollMainDialogFragment.newInstance( + message.activeUser!!, + roomToken, + isOwnerOrModerator, + pollId, + pollName + ) + pollVoteDialog.show( + (binding.messagePollIcon.context as ChatActivity).supportFragmentManager, + TAG + ) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod") + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + false, + viewThemeUtils + ) + viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) + viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + View.VISIBLE + } else { + View.GONE + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted) + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + companion object { + private val TAG = OutcomingPollMessageViewHolder::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java new file mode 100644 index 0000000..17b1c55 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java @@ -0,0 +1,143 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages; + +import android.text.Spanned; +import android.util.TypedValue; +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; + +import com.google.android.material.card.MaterialCardView; +import com.nextcloud.talk.R; +import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding; +import com.nextcloud.talk.databinding.ItemThreadTitleBinding; +import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding; +import com.nextcloud.talk.chat.data.model.ChatMessage; +import com.nextcloud.talk.utils.TextMatchers; + +import java.util.HashMap; +import java.util.Objects; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.emoji2.widget.EmojiTextView; + +public class OutcomingPreviewMessageViewHolder extends PreviewMessageViewHolder { + + private final ItemCustomOutcomingPreviewMessageBinding binding; + + public OutcomingPreviewMessageViewHolder(View itemView) { + super(itemView, null); + binding = ItemCustomOutcomingPreviewMessageBinding.bind(itemView); + } + + @Override + public void onBind(@NonNull ChatMessage message) { + super.onBind(message); + if(!message.isVoiceMessage() + && !Objects.equals(message.getMessage(), "{file}") + ) { + Spanned processedMessageText = null; + binding.outgoingPreviewMessageBubble.setBackgroundResource(R.drawable.shape_grouped_outcoming_message); + if (viewThemeUtils != null) { + processedMessageText = messageUtils.enrichChatMessageText( + binding.messageCaption.getContext(), + message, + false, + viewThemeUtils); + viewThemeUtils.talk.themeOutgoingMessageBubble(binding.outgoingPreviewMessageBubble, true, false, + false); + } + + if (processedMessageText != null) { + processedMessageText = messageUtils.processMessageParameters( + binding.messageCaption.getContext(), + viewThemeUtils, + processedMessageText, + message, + binding.outgoingPreviewMessageBubble); + } + binding.outgoingPreviewMessageBubble.setOnClickListener(null); + + float textSize = 0; + if (context != null) { + textSize = context.getResources().getDimension(R.dimen.chat_text_size); + } + HashMap> messageParameters = message.getMessageParameters(); + if ( + (messageParameters == null || messageParameters.size() <= 0) && + TextMatchers.isMessageWithSingleEmoticonOnly(message.getText()) + ) { + textSize = (float)(textSize * IncomingTextMessageViewHolder.TEXT_SIZE_MULTIPLIER); + itemView.setSelected(true); + } + binding.messageCaption.setVisibility(View.VISIBLE); + binding.messageCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + binding.messageCaption.setText(processedMessageText); + } else { + binding.outgoingPreviewMessageBubble.setBackground(null); + binding.messageCaption.setVisibility(View.GONE); + } + + binding.messageText.setTextColor(ContextCompat.getColor(binding.messageText.getContext(), + R.color.no_emphasis_text)); + binding.messageTime.setTextColor(ContextCompat.getColor(binding.messageText.getContext(), + R.color.no_emphasis_text)); + } + + @NonNull + @Override + public EmojiTextView getMessageText() { + return binding.messageText; + } + + @Override + public ProgressBar getProgressBar() { + return binding.progressBar; + } + + @NonNull + @Override + public View getPreviewContainer() { + return binding.previewContainer; + } + + @NonNull + @Override + public MaterialCardView getPreviewContactContainer() { + return binding.contactContainer; + } + + @NonNull + @Override + public ImageView getPreviewContactPhoto() { + return binding.contactPhoto; + } + + @NonNull + @Override + public EmojiTextView getPreviewContactName() { + return binding.contactName; + } + + @Override + public ProgressBar getPreviewContactProgressBar() { + return binding.contactProgressBar; + } + + @Override + public ReactionsInsideMessageBinding getReactionsBinding() { return binding.reactions; } + + @Override + public ItemThreadTitleBinding getThreadsBinding(){ return binding.threadTitleWrapper; } + + @NonNull + @Override + public EmojiTextView getMessageCaption() { return binding.messageCaption; } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt new file mode 100644 index 0000000..755d32a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingTextMessageViewHolder.kt @@ -0,0 +1,452 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.content.Context +import android.util.Log +import android.util.TypedValue +import android.view.View +import android.widget.CheckBox +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.text.toSpanned +import androidx.lifecycle.lifecycleScope +import autodagger.AutoInjector +import coil.load +import com.google.android.flexbox.FlexboxLayout +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.ChatMessageRepository +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.database.model.SendStatus +import com.nextcloud.talk.data.network.NetworkMonitor +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.TextMatchers +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.message.MessageUtils +import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.Date +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class OutcomingTextMessageViewHolder(itemView: View) : + OutcomingTextMessageViewHolder(itemView), + AdjustableMessageHolderInterface { + + override val binding: ItemCustomOutcomingTextMessageBinding = ItemCustomOutcomingTextMessageBinding.bind(itemView) + private val realView: View = itemView + + @Inject + lateinit var context: Context + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var networkMonitor: NetworkMonitor + + lateinit var commonMessageInterface: CommonMessageInterface + + @Inject + lateinit var chatRepository: ChatMessageRepository + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + private var job: Job? = null + + @Suppress("Detekt.LongMethod") + override fun onBind(message: ChatMessage) { + super.onBind(message) + sharedApplication!!.componentApplication.inject(this) + val user = currentUserProvider.currentUser.blockingGet() + val hasCheckboxes = processCheckboxes( + message, + user + ) + processMessage(message, hasCheckboxes) + } + + @Suppress("Detekt.LongMethod") + private fun processMessage(message: ChatMessage, hasCheckboxes: Boolean) { + var isBubbled = true + val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams + var textSize = context.resources.getDimension(R.dimen.chat_text_size) + if (!hasCheckboxes) { + realView.isSelected = false + layoutParams.isWrapBefore = false + + binding.messageText.visibility = View.VISIBLE + binding.checkboxContainer.visibility = View.GONE + + var processedMessageText = messageUtils.enrichChatMessageText( + binding.messageText.context, + message, + false, + viewThemeUtils + ) + + val spansFromString: Array = processedMessageText!!.getSpans( + 0, + processedMessageText.length, + Any::class.java + ) + + if (spansFromString.isNotEmpty()) { + binding.bubble.layoutParams.apply { + width = FlexboxLayout.LayoutParams.MATCH_PARENT + } + binding.messageText.layoutParams.apply { + width = FlexboxLayout.LayoutParams.MATCH_PARENT + } + } else { + binding.bubble.layoutParams.apply { + width = FlexboxLayout.LayoutParams.WRAP_CONTENT + } + binding.messageText.layoutParams.apply { + width = FlexboxLayout.LayoutParams.WRAP_CONTENT + } + } + + processedMessageText = messageUtils.processMessageParameters( + binding.messageText.context, + viewThemeUtils, + processedMessageText, + message, + itemView + ) + + if ( + (message.messageParameters == null || message.messageParameters!!.size <= 0) && + TextMatchers.isMessageWithSingleEmoticonOnly(message.text) + ) { + textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat() + layoutParams.isWrapBefore = true + realView.isSelected = true + isBubbled = false + } + + binding.messageTime.layoutParams = layoutParams + viewThemeUtils.platform.colorTextView(binding.messageText, ColorRole.ON_SURFACE_VARIANT) + binding.messageText.text = processedMessageText + // just for debugging: + // binding.messageText.text = + // SpannableStringBuilder(processedMessageText).append(" (" + message.jsonMessageId + ")") + } else { + binding.messageText.visibility = View.GONE + binding.checkboxContainer.visibility = View.VISIBLE + } + binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + if (message.lastEditTimestamp != 0L && !message.isDeleted) { + binding.messageEditIndicator.visibility = View.VISIBLE + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!) + } else { + binding.messageEditIndicator.visibility = View.GONE + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + } + viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) + setBubbleOnChatMessage(message) + + // parent message handling + val chatActivity = commonMessageInterface as ChatActivity + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + processParentMessage(message) + View.VISIBLE + } else { + View.GONE + } + + binding.messageQuote.quotedChatMessageView.setOnLongClickListener { l: View? -> + commonMessageInterface.onOpenMessageActionsDialog(message) + true + } + + binding.checkMark.visibility = View.INVISIBLE + binding.sendingProgress.visibility = View.GONE + + if (message.sendStatus == SendStatus.FAILED) { + updateStatus(R.drawable.baseline_error_outline_24, context.resources?.getString(R.string.nc_message_failed)) + } else if (message.isTemporary) { + updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending)) + } else if (message.readStatus == ReadStatus.READ) { + updateStatus(R.drawable.ic_check_all, context.resources?.getString(R.string.nc_message_read)) + } else if (message.readStatus == ReadStatus.SENT) { + updateStatus(R.drawable.ic_check, context.resources?.getString(R.string.nc_message_sent)) + } + + chatActivity.lifecycleScope.launch { + if (message.isTemporary && !networkMonitor.isOnline.value) { + updateStatus( + R.drawable.ic_signal_wifi_off_white_24dp, + context.resources?.getString(R.string.nc_message_offline) + ) + } + } + + itemView.setTag(R.string.replyable_message_view_tag, message.replyable) + + Thread().showThreadPreview( + chatActivity, + message, + threadBinding = binding.threadTitleWrapper, + reactionsBinding = binding.reactions, + openThread = { openThread(message) } + ) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + context, + true, + viewThemeUtils, + isBubbled + ) + } + + private fun processCheckboxes(chatMessage: ChatMessage, user: User): Boolean { + val chatActivity = commonMessageInterface as ChatActivity + val message = chatMessage.message!!.toSpanned() + val messageTextView = binding.messageText + val checkBoxContainer = binding.checkboxContainer + val isOlderThanTwentyFourHours = chatMessage + .createdAt + .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE)) + val messageIsEditable = hasSpreedFeatureCapability( + user.capabilities?.spreedCapability!!, + SpreedFeatures.EDIT_MESSAGES + ) && + !isOlderThanTwentyFourHours + + val isNoTimeLimitOnNoteToSelf = hasSpreedFeatureCapability( + user.capabilities?.spreedCapability!!, + SpreedFeatures + .EDIT_MESSAGES_NOTE_TO_SELF + ) && + chatActivity.currentConversation?.type == ConversationEnums.ConversationType.NOTE_TO_SELF + + checkBoxContainer.removeAllViews() + val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE) + val matches = regex.findAll(message) + + if (matches.none()) return false + + val firstPart = message.toString().substringBefore("\n- [") + messageTextView.text = messageUtils.enrichChatMessageText( + binding.messageText.context, + firstPart, + true, + viewThemeUtils + ) + + val checkboxList = mutableListOf() + + matches.forEach { matchResult -> + val isChecked = matchResult.groupValues[CHECKED_GROUP_INDEX] == "X" || + matchResult.groupValues[CHECKED_GROUP_INDEX] == "x" + val taskText = matchResult.groupValues[TASK_TEXT_GROUP_INDEX].trim() + + val checkBox = CheckBox(checkBoxContainer.context).apply { + text = taskText + this.isChecked = isChecked + this.isEnabled = messageIsEditable || isNoTimeLimitOnNoteToSelf + + setTextColor(ContextCompat.getColor(context, R.color.no_emphasis_text)) + + setOnCheckedChangeListener { _, _ -> + updateCheckboxStates(chatMessage, user, checkboxList) + } + } + checkBoxContainer.addView(checkBox) + checkboxList.add(checkBox) + viewThemeUtils.platform.themeCheckbox(checkBox) + } + return true + } + + private fun updateCheckboxStates(chatMessage: ChatMessage, user: User, checkboxes: List) { + job = CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.IO) { + val apiVersion: Int = ApiUtils.getChatApiVersion( + user.capabilities?.spreedCapability!!, + intArrayOf(1) + ) + val updatedMessage = updateMessageWithCheckboxStates(chatMessage.message!!, checkboxes) + chatRepository.editChatMessage( + user.getCredentials(), + ApiUtils.getUrlForChatMessage(apiVersion, user.baseUrl!!, chatMessage.token!!, chatMessage.id), + updatedMessage + ).collect { result -> + withContext(Dispatchers.Main) { + if (result.isSuccess) { + val editedMessage = result.getOrNull()?.ocs?.data!!.parentMessage!! + Log.d(TAG, "EditedMessage: $editedMessage") + binding.messageEditIndicator.apply { + visibility = View.VISIBLE + } + binding.messageTime.text = + dateUtils.getLocalTimeStringFromTimestamp(editedMessage.lastEditTimestamp!!) + } else { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + } + } + } + } + } + + private fun updateMessageWithCheckboxStates(originalMessage: String, checkboxes: List): String { + var updatedMessage = originalMessage + val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE) + + checkboxes.forEach { _ -> + updatedMessage = regex.replace(updatedMessage) { matchResult -> + val taskText = matchResult.groupValues[TASK_TEXT_GROUP_INDEX].trim() + val checkboxState = if (checkboxes.find { it.text == taskText }?.isChecked == true) "X" else " " + "- [$checkboxState] $taskText" + } + } + return updatedMessage + } + + private fun updateStatus(readStatusDrawableInt: Int, description: String?) { + binding.sendingProgress.visibility = View.GONE + binding.checkMark.visibility = View.VISIBLE + readStatusDrawableInt.let { drawableInt -> + ResourcesCompat.getDrawable(context.resources, drawableInt, null)?.let { + binding.checkMark.setImageDrawable(it) + viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark) + } + } + binding.checkMark.contentDescription = description + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun openThread(chatMessage: ChatMessage) { + commonMessageInterface.openThread(chatMessage) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun processParentMessage(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + false, + viewThemeUtils + ) + + viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) + viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + + binding.messageQuote.quotedChatMessageView.setOnClickListener { + chatActivity.jumpToQuotedMessage(parentChatMessage) + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } + } + + private fun setBubbleOnChatMessage(message: ChatMessage) { + viewThemeUtils.talk.themeOutgoingMessageBubble(bubble, message.isGrouped, message.isDeleted) + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + override fun viewDetached() { + super.viewDetached() + job?.cancel() + } + + companion object { + const val TEXT_SIZE_MULTIPLIER = 2.5 + private val TAG = OutcomingTextMessageViewHolder::class.java.simpleName + private const val CHECKED_GROUP_INDEX = 2 + private const val TASK_TEXT_GROUP_INDEX = 3 + private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt new file mode 100644 index 0000000..09ef5c0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt @@ -0,0 +1,398 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Handler +import android.util.Log +import android.view.View +import android.widget.SeekBar +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import coil.load +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ExecutionException +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +@Suppress("Detekt.TooManyFunctions") +class OutcomingVoiceMessageViewHolder(outcomingView: View) : + MessageHolders.OutcomingTextMessageViewHolder(outcomingView), + AdjustableMessageHolderInterface { + + override val binding: ItemCustomOutcomingVoiceMessageBinding = ItemCustomOutcomingVoiceMessageBinding.bind(itemView) + + @JvmField + @Inject + var context: Context? = null + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var appPreferences: AppPreferences + + lateinit var message: ChatMessage + + lateinit var handler: Handler + + lateinit var voiceMessageInterface: VoiceMessageInterface + lateinit var commonMessageInterface: CommonMessageInterface + private var isBound = false + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + if (isBound) { + handleIsPlayingVoiceMessageState(message) + return + } + + this.message = message + sharedApplication!!.componentApplication.inject(this) + viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT) + + val filename = message.selectedIndividualHashMap!!["name"] + val retrieved = appPreferences.getWaveFormFromFile(filename) + if (retrieved.isNotEmpty() && + message.voiceMessageFloatArray == null || + message.voiceMessageFloatArray?.isEmpty() == true + ) { + message.voiceMessageFloatArray = retrieved.toFloatArray() + binding.seekbar.setWaveData(message.voiceMessageFloatArray!!) + } + + binding.seekbar.max = MAX + binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + colorizeMessageBubble(message) + + itemView.isSelected = false + + // parent message handling + setParentMessageDataOnMessageItem(message) + + updateDownloadState(message) + viewThemeUtils.talk.themeWaveFormSeekBar(binding.seekbar) + viewThemeUtils.platform.colorCircularProgressBar(binding.progressBar, ColorRole.ON_SURFACE_VARIANT) + + showVoiceMessageDuration(message) + + handleIsDownloadingVoiceMessageState(message) + + handleResetVoiceMessageState(message) + + binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onStopTrackingTouch(seekBar: SeekBar) { + // unused atm + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + // unused atm + } + + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (fromUser) { + voiceMessageInterface.updateMediaPlayerProgressBySlider(message, progress) + } + } + }) + + setReadStatus(message.readStatus) + + CoroutineScope(Dispatchers.Default).launch { + (voiceMessageInterface as ChatActivity).chatViewModel.voiceMessagePlayBackUIFlow.onEach { speed -> + withContext(Dispatchers.Main) { + binding.playbackSpeedControlBtn.setSpeed(speed) + } + }.collect() + } + + binding.playbackSpeedControlBtn.setSpeed(appPreferences.getPreferredPlayback(message.actorId)) + + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + binding.reactions, + binding.messageTime.context, + true, + viewThemeUtils + ) + isBound = true + } + + private fun setReadStatus(readStatus: Enum) { + val readStatusDrawableInt = when (readStatus) { + ReadStatus.READ -> R.drawable.ic_check_all + ReadStatus.SENT -> R.drawable.ic_check + else -> null + } + + val readStatusContentDescriptionString = when (readStatus) { + ReadStatus.READ -> context?.resources?.getString(R.string.nc_message_read) + ReadStatus.SENT -> context?.resources?.getString(R.string.nc_message_sent) + else -> null + } + + readStatusDrawableInt?.let { drawableInt -> + AppCompatResources.getDrawable(context!!, drawableInt)?.let { + binding.checkMark.setImageDrawable(it) + viewThemeUtils.talk.themeMessageCheckMark(binding.checkMark) + } + } + + binding.checkMark.contentDescription = readStatusContentDescriptionString + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun handleResetVoiceMessageState(message: ChatMessage) { + if (message.resetVoiceMessage) { + binding.playPauseBtn.visibility = View.VISIBLE + binding.playPauseBtn.icon = ContextCompat.getDrawable( + context!!, + R.drawable.ic_baseline_play_arrow_voice_message_24 + ) + binding.seekbar.progress = SEEKBAR_START + message.voiceMessagePlayedSeconds = 0 + showVoiceMessageDuration(message) + message.resetVoiceMessage = false + } + } + + private fun showVoiceMessageDuration(message: ChatMessage) { + if (message.voiceMessageDuration > 0) { + binding.voiceMessageDuration.visibility = View.VISIBLE + } else { + binding.voiceMessageDuration.visibility = View.INVISIBLE + } + } + + private fun handleIsDownloadingVoiceMessageState(message: ChatMessage) { + if (message.isDownloadingVoiceMessage) { + showVoiceMessageLoading() + } else { + if (message.voiceMessageFloatArray == null || message.voiceMessageFloatArray!!.isEmpty()) { + binding.seekbar.setWaveData(FloatArray(0)) + } else { + binding.seekbar.setWaveData(message.voiceMessageFloatArray!!) + } + binding.progressBar.visibility = View.GONE + } + } + + private fun handleIsPlayingVoiceMessageState(message: ChatMessage) { + colorizeMessageBubble(message) + if (message.isPlayingVoiceMessage) { + showPlayButton() + binding.playPauseBtn.icon = ContextCompat.getDrawable( + context!!, + R.drawable.ic_baseline_pause_voice_message_24 + ) + + val d = message.voiceMessageDuration.toLong() + val t = message.voiceMessagePlayedSeconds.toLong() + binding.voiceMessageDuration.text = android.text.format.DateUtils.formatElapsedTime(d - t) + binding.voiceMessageDuration.visibility = View.VISIBLE + binding.seekbar.progress = message.voiceMessageSeekbarProgress + } else { + showVoiceMessageDuration(message) + binding.playPauseBtn.visibility = View.VISIBLE + binding.playPauseBtn.icon = ContextCompat.getDrawable( + context!!, + R.drawable.ic_baseline_play_arrow_voice_message_24 + ) + } + } + + private fun updateDownloadState(message: ChatMessage) { + // check if download worker is already running + val fileId = message.selectedIndividualHashMap!!["id"] + val workers = WorkManager.getInstance(context!!).getWorkInfosByTag(fileId!!) + + try { + for (workInfo in workers.get()) { + if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { + showVoiceMessageLoading() + WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id) + .observeForever { info: WorkInfo? -> + updateDownloadState(info) + } + } + } + } catch (e: ExecutionException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } catch (e: InterruptedException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } + } + + private fun updateDownloadState(info: WorkInfo?) { + if (info != null) { + when (info.state) { + WorkInfo.State.RUNNING -> { + Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder") + showVoiceMessageLoading() + } + + WorkInfo.State.SUCCEEDED -> { + Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder") + showPlayButton() + } + + WorkInfo.State.FAILED -> { + Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder") + showPlayButton() + } + + else -> { + Log.d(TAG, "WorkInfo.State unused in ViewHolder") + } + } + } + } + + private fun showPlayButton() { + binding.playPauseBtn.visibility = View.VISIBLE + binding.progressBar.visibility = View.GONE + } + + private fun showVoiceMessageLoading() { + binding.playPauseBtn.visibility = View.GONE + binding.progressBar.visibility = View.VISIBLE + } + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.LongMethod") + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (message.parentMessageId != null && !message.isDeleted) { + CoroutineScope(Dispatchers.Main).launch { + try { + val chatActivity = commonMessageInterface as ChatActivity + val urlForChatting = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser?.baseUrl, + chatActivity.roomToken + ) + + val parentChatMessage = withContext(Dispatchers.IO) { + chatActivity.chatViewModel.getMessageById( + urlForChatting, + chatActivity.currentConversation!!, + message.parentMessageId!! + ).first() + } + parentChatMessage.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context!!.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = messageUtils + .enrichChatReplyMessageText( + binding.messageQuote.quotedMessage.context, + parentChatMessage, + false, + viewThemeUtils + ) + viewThemeUtils.talk.colorOutgoingQuoteText(binding.messageQuote.quotedMessage) + viewThemeUtils.talk.colorOutgoingQuoteAuthorText(binding.messageQuote.quotedMessageAuthor) + viewThemeUtils.talk.themeParentMessage( + parentChatMessage, + message, + binding.messageQuote.quotedChatMessageView + ) + + binding.messageQuote.quotedChatMessageView.visibility = + if (!message.isDeleted && + message.parentMessageId != null && + message.parentMessageId != chatActivity.conversationThreadId + ) { + View.VISIBLE + } else { + View.GONE + } + } catch (e: Exception) { + Log.d(TAG, "Error when processing parent message in view holder", e) + } + } + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + viewThemeUtils.talk.themeOutgoingMessageBubble( + bubble, + message.isGrouped, + message.isDeleted, + message.wasPlayedVoiceMessage + ) + } + + fun assignVoiceMessageInterface(voiceMessageInterface: VoiceMessageInterface) { + this.voiceMessageInterface = voiceMessageInterface + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + companion object { + private const val TAG = "VoiceOutMessageView" + private const val SEEKBAR_START: Int = 0 + private const val MAX = 100 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageInterface.kt new file mode 100644 index 0000000..c7358b9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageInterface.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import com.nextcloud.talk.chat.data.model.ChatMessage + +interface PreviewMessageInterface { + fun onPreviewMessageLongClick(chatMessage: ChatMessage) +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt new file mode 100644 index 0000000..9449526 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt @@ -0,0 +1,359 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Tim Krüger + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.os.Handler +import android.util.Base64 +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.ProgressBar +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.emoji2.widget.EmojiTextView +import autodagger.AutoInjector +import com.google.android.material.card.MaterialCardView +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ItemThreadTitleBinding +import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding +import com.nextcloud.talk.extensions.loadChangelogBotAvatar +import com.nextcloud.talk.extensions.loadFederatedUserAvatar +import com.nextcloud.talk.filebrowser.models.BrowserFile +import com.nextcloud.talk.filebrowser.webdav.ReadFilesystemOperation +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType +import com.nextcloud.talk.utils.FileViewerUtils +import com.nextcloud.talk.utils.FileViewerUtils.ProgressUi +import com.nextcloud.talk.utils.message.MessageUtils +import com.stfalcon.chatkit.messages.MessageHolders.IncomingImageMessageViewHolder +import io.reactivex.Single +import io.reactivex.SingleObserver +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import okhttp3.OkHttpClient +import java.io.ByteArrayInputStream +import java.io.IOException +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : + IncomingImageMessageViewHolder(itemView, payload) { + @JvmField + @Inject + var context: Context? = null + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var messageUtils: MessageUtils + + @Inject + lateinit var userManager: UserManager + + @JvmField + @Inject + var okHttpClient: OkHttpClient? = null + open var progressBar: ProgressBar? = null + open var reactionsBinding: ReactionsInsideMessageBinding? = null + open var threadsBinding: ItemThreadTitleBinding? = null + var fileViewerUtils: FileViewerUtils? = null + var clickView: View? = null + + lateinit var commonMessageInterface: CommonMessageInterface + var previewMessageInterface: PreviewMessageInterface? = null + + private var placeholder: Drawable? = null + + init { + sharedApplication!!.componentApplication.inject(this) + } + + @SuppressLint("SetTextI18n") + @Suppress("NestedBlockDepth", "ComplexMethod", "LongMethod") + override fun onBind(message: ChatMessage) { + super.onBind(message) + image.minimumHeight = DisplayUtils.convertDpToPixel(MIN_IMAGE_HEIGHT, context!!).toInt() + + time.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) + + viewThemeUtils!!.platform.colorCircularProgressBar(progressBar!!, ColorRole.PRIMARY) + clickView = image + messageText.visibility = View.VISIBLE + if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) { + fileViewerUtils = FileViewerUtils(context!!, message.activeUser!!) + val fileName = message.selectedIndividualHashMap!![KEY_NAME] + + messageText.text = fileName + + if (message.activeUser != null && + message.activeUser!!.username != null && + message.activeUser!!.baseUrl != null + ) { + clickView!!.setOnClickListener { v: View? -> + fileViewerUtils!!.openFile( + message, + ProgressUi(progressBar, messageText, image) + ) + } + clickView!!.setOnLongClickListener { + previewMessageInterface!!.onPreviewMessageLongClick(message) + true + } + } else { + Log.e(TAG, "failed to set click listener because activeUser, username or baseUrl were null") + } + fileViewerUtils!!.resumeToUpdateViewsByProgress( + message.selectedIndividualHashMap!![KEY_NAME]!!, + message.selectedIndividualHashMap!![KEY_ID]!!, + message.selectedIndividualHashMap!![KEY_MIMETYPE], + message.openWhenDownloaded, + ProgressUi(progressBar, messageText, image) + ) + } else if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE) { + messageText.text = "GIPHY" + DisplayUtils.setClickableString("GIPHY", "https://giphy.com", messageText) + } else if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE) { + messageText.text = "Tenor" + DisplayUtils.setClickableString("Tenor", "https://tenor.com", messageText) + } else { + if (message.messageType == ChatMessage.MessageType.SINGLE_LINK_IMAGE_MESSAGE.name) { + clickView!!.setOnClickListener { + val browserIntent = Intent(Intent.ACTION_VIEW, message.imageUrl!!.toUri()) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context!!.startActivity(browserIntent) + } + } else { + clickView!!.setOnClickListener(null) + } + messageText.text = "" + } + itemView.setTag(R.string.replyable_message_view_tag, message.replyable) + + val chatActivity = commonMessageInterface as ChatActivity + Thread().showThreadPreview( + chatActivity, + message, + threadBinding = threadsBinding!!, + reactionsBinding = reactionsBinding!!, + openThread = { openThread(message) } + ) + + val paddingSide = DisplayUtils.convertDpToPixel(HORIZONTAL_REACTION_PADDING, context!!).toInt() + Reaction().showReactions( + message, + ::clickOnReaction, + ::longClickOnReaction, + reactionsBinding!!, + messageText.context, + true, + viewThemeUtils!!, + hasBubbleBackground(message) + ) + reactionsBinding!!.reactionsEmojiWrapper.setPadding(paddingSide, 0, paddingSide, 0) + + if (userAvatar != null) { + if (message.isGrouped || message.isOneToOneConversation) { + if (message.isOneToOneConversation) { + userAvatar.visibility = View.GONE + } else { + userAvatar.visibility = View.INVISIBLE + } + } else { + userAvatar.visibility = View.VISIBLE + userAvatar.setOnClickListener { v: View -> + if (payload is MessagePayload) { + (payload as MessagePayload).profileBottomSheet.showFor( + message, + v.context + ) + } + } + if (ACTOR_TYPE_BOTS == message.actorType && ACTOR_ID_CHANGELOG == message.actorId) { + userAvatar.loadChangelogBotAvatar() + } else if (message.actorType == "federated_users") { + userAvatar.loadFederatedUserAvatar(message) + } + } + } + + messageCaption.setOnClickListener(null) + messageCaption.setOnLongClickListener { + previewMessageInterface!!.onPreviewMessageLongClick(message) + true + } + } + + private fun longClickOnReaction(chatMessage: ChatMessage) { + commonMessageInterface.onLongClickReactions(chatMessage) + } + + private fun clickOnReaction(chatMessage: ChatMessage, emoji: String) { + commonMessageInterface.onClickReaction(chatMessage, emoji) + } + + private fun openThread(chatMessage: ChatMessage) { + commonMessageInterface.openThread(chatMessage) + } + + override fun getPayloadForImageLoader(message: ChatMessage?): Any? { + if (message!!.selectedIndividualHashMap!!.containsKey(KEY_CONTACT_NAME)) { + previewContainer.visibility = View.GONE + previewContactContainer.visibility = View.VISIBLE + previewContactName.text = message.selectedIndividualHashMap!![KEY_CONTACT_NAME] + progressBar = previewContactProgressBar + messageText.visibility = View.INVISIBLE + clickView = previewContactContainer + viewThemeUtils!!.talk.colorContactChatItemBackground(previewContactContainer) + viewThemeUtils!!.talk.colorContactChatItemName(previewContactName) + viewThemeUtils!!.platform.colorCircularProgressBar( + previewContactProgressBar!!, + ColorRole.ON_PRIMARY_CONTAINER + ) + + if (message.selectedIndividualHashMap!!.containsKey(KEY_CONTACT_PHOTO)) { + image = previewContactPhoto + placeholder = getDrawableFromContactDetails( + context, + message.selectedIndividualHashMap!![KEY_CONTACT_PHOTO] + ) + } else { + image = previewContactPhoto + image.setImageDrawable(ContextCompat.getDrawable(context!!, R.drawable.ic_mimetype_text_vcard)) + } + } else { + previewContainer.visibility = View.VISIBLE + previewContactContainer.visibility = View.GONE + } + + if (message.selectedIndividualHashMap!!.containsKey(KEY_MIMETYPE)) { + val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE] + val drawableResourceId = getDrawableResourceIdForMimeType(mimetype) + var drawable = ContextCompat.getDrawable(context!!, drawableResourceId) + if (drawable != null && + ( + drawableResourceId == R.drawable.ic_mimetype_folder || + drawableResourceId == R.drawable.ic_mimetype_package_x_generic + ) + ) { + drawable = viewThemeUtils?.platform?.tintDrawable(context!!, drawable) + } + placeholder = drawable + } else { + fetchFileInformation( + "/" + message.selectedIndividualHashMap!![KEY_PATH], + message.activeUser + ) + } + + return placeholder + } + + private fun getDrawableFromContactDetails(context: Context?, base64: String?): Drawable? { + var drawable: Drawable? = null + if (base64 != "") { + val inputStream = ByteArrayInputStream( + Base64.decode(base64!!.toByteArray(), Base64.DEFAULT) + ) + drawable = Drawable.createFromResourceStream( + context!!.resources, + null, + inputStream, + null, + null + ) + try { + inputStream.close() + } catch (e: IOException) { + Log.e(TAG, "failed to close stream in getDrawableFromContactDetails", e) + } + } + if (drawable == null) { + drawable = ContextCompat.getDrawable(context!!, R.drawable.ic_mimetype_text_vcard) + } + return drawable + } + + private fun fetchFileInformation(url: String, activeUser: User?) { + Single.fromCallable { ReadFilesystemOperation(okHttpClient, activeUser, url, 0) } + .observeOn(Schedulers.io()) + .subscribe(object : SingleObserver { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onSuccess(readFilesystemOperation: ReadFilesystemOperation) { + val davResponse = readFilesystemOperation.readRemotePath() + if (davResponse.data != null) { + val browserFileList = davResponse.data as List + if (browserFileList.isNotEmpty()) { + Handler(context!!.mainLooper).post { + val resourceId = getDrawableResourceIdForMimeType(browserFileList[0].mimeType) + placeholder = ContextCompat.getDrawable(context!!, resourceId) + } + } + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error reading file information", e) + } + }) + } + + fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) { + this.commonMessageInterface = commonMessageInterface + } + + fun assignPreviewMessageInterface(previewMessageInterface: PreviewMessageInterface?) { + this.previewMessageInterface = previewMessageInterface + } + + fun hasBubbleBackground(message: ChatMessage): Boolean = !message.isVoiceMessage && message.message != "{file}" + + abstract val messageText: EmojiTextView + abstract val messageCaption: EmojiTextView + abstract val previewContainer: View + abstract val previewContactContainer: MaterialCardView + abstract val previewContactPhoto: ImageView + abstract val previewContactName: EmojiTextView + abstract val previewContactProgressBar: ProgressBar? + + companion object { + private const val TAG = "PreviewMsgViewHolder" + const val KEY_CONTACT_NAME = "contact-name" + const val KEY_CONTACT_PHOTO = "contact-photo" + const val KEY_MIMETYPE = "mimetype" + const val KEY_ID = "id" + const val KEY_PATH = "path" + const val ACTOR_TYPE_BOTS = "bots" + const val ACTOR_ID_CHANGELOG = "changelog" + const val KEY_NAME = "name" + const val MIN_IMAGE_HEIGHT = 100F + const val HORIZONTAL_REACTION_PADDING = 8.0F + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt new file mode 100644 index 0000000..d348993 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt @@ -0,0 +1,181 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.vanniktech.emoji.EmojiTextView + +class Reaction { + + fun showReactions( + message: ChatMessage, + clickOnReaction: (message: ChatMessage, emoji: String) -> Unit, + longClickOnReaction: (message: ChatMessage) -> Unit, + binding: ReactionsInsideMessageBinding, + context: Context, + isOutgoingMessage: Boolean, + viewThemeUtils: ViewThemeUtils, + isBubbled: Boolean = true + ) { + binding.reactionsEmojiWrapper.removeAllViews() + + if (message.reactions != null && message.reactions!!.isNotEmpty()) { + binding.reactionsEmojiWrapper.visibility = View.VISIBLE + + binding.reactionsEmojiWrapper.setOnLongClickListener { + longClickOnReaction(message) + true + } + + val amountParams = getAmountLayoutParams(context) + val wrapperParams = getWrapperLayoutParams(context) + + val paddingSide = DisplayUtils.convertDpToPixel(EMOJI_AND_AMOUNT_PADDING_SIDE, context).toInt() + val paddingTop = DisplayUtils.convertDpToPixel(WRAPPER_PADDING_TOP, context).toInt() + val paddingBottom = DisplayUtils.convertDpToPixel(WRAPPER_PADDING_BOTTOM, context).toInt() + + for ((emoji, amount) in message.reactions!!) { + val isSelfReaction = message.reactionsSelf != null && + message.reactionsSelf!!.isNotEmpty() && + message.reactionsSelf!!.contains(emoji) + val textColor = viewThemeUtils.talk.getTextColor(isOutgoingMessage, isSelfReaction, binding) + val emojiWithAmountWrapper = getEmojiWithAmountWrapperLayout( + binding.reactionsEmojiWrapper.context, + emoji, + amount, + EmojiWithAmountWrapperLayoutInfo( + textColor, + amountParams, + wrapperParams, + paddingSide, + paddingTop, + paddingBottom, + viewThemeUtils, + isOutgoingMessage, + isSelfReaction + ), + isBubbled + ) + + emojiWithAmountWrapper.setOnClickListener { + clickOnReaction(message, emoji) + } + emojiWithAmountWrapper.setOnLongClickListener { + longClickOnReaction(message) + false + } + + binding.reactionsEmojiWrapper.addView(emojiWithAmountWrapper) + } + } else { + binding.reactionsEmojiWrapper.visibility = View.GONE + } + } + + private fun getEmojiWithAmountWrapperLayout( + context: Context, + emoji: String, + amount: Int, + layoutInfo: EmojiWithAmountWrapperLayoutInfo, + isBubbled: Boolean + ): LinearLayout { + val emojiWithAmountWrapper = LinearLayout(context) + emojiWithAmountWrapper.orientation = LinearLayout.HORIZONTAL + + emojiWithAmountWrapper.addView(getEmojiTextView(context, emoji)) + emojiWithAmountWrapper.addView(getReactionCount(context, layoutInfo.textColor, amount, layoutInfo.amountParams)) + emojiWithAmountWrapper.layoutParams = layoutInfo.wrapperParams + + if (layoutInfo.isSelfReaction) { + layoutInfo.viewThemeUtils.talk.setCheckedBackground( + emojiWithAmountWrapper, + layoutInfo.isOutgoingMessage, + isBubbled + ) + + emojiWithAmountWrapper.setPaddingRelative( + layoutInfo.paddingSide, + layoutInfo.paddingTop, + layoutInfo.paddingSide, + layoutInfo.paddingBottom + ) + } else { + emojiWithAmountWrapper.setPaddingRelative( + 0, + layoutInfo.paddingTop, + layoutInfo.paddingSide, + layoutInfo.paddingBottom + ) + } + return emojiWithAmountWrapper + } + + private fun getEmojiTextView(context: Context, emoji: String): EmojiTextView { + val reactionEmoji = EmojiTextView(context) + reactionEmoji.text = emoji + return reactionEmoji + } + + private fun getReactionCount( + context: Context, + textColor: Int, + amount: Int, + amountParams: LinearLayout.LayoutParams + ): TextView { + val reactionAmount = TextView(context) + reactionAmount.setTextColor(textColor) + reactionAmount.text = amount.toString() + reactionAmount.layoutParams = amountParams + return reactionAmount + } + + private fun getWrapperLayoutParams(context: Context): LinearLayout.LayoutParams { + val wrapperParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + wrapperParams.marginEnd = DisplayUtils.convertDpToPixel(EMOJI_END_MARGIN, context).toInt() + return wrapperParams + } + + private fun getAmountLayoutParams(context: Context): LinearLayout.LayoutParams { + val amountParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + amountParams.marginStart = DisplayUtils.convertDpToPixel(AMOUNT_START_MARGIN, context).toInt() + return amountParams + } + + private data class EmojiWithAmountWrapperLayoutInfo( + val textColor: Int, + val amountParams: LinearLayout.LayoutParams, + val wrapperParams: LinearLayout.LayoutParams, + val paddingSide: Int, + val paddingTop: Int, + val paddingBottom: Int, + val viewThemeUtils: ViewThemeUtils, + val isOutgoingMessage: Boolean, + val isSelfReaction: Boolean + ) + + companion object { + const val AMOUNT_START_MARGIN: Float = 2F + const val EMOJI_END_MARGIN: Float = 6F + const val EMOJI_AND_AMOUNT_PADDING_SIDE: Float = 4F + const val WRAPPER_PADDING_TOP: Float = 2F + const val WRAPPER_PADDING_BOTTOM: Float = 3F + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt new file mode 100644 index 0000000..18199b7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageInterface.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import com.nextcloud.talk.chat.data.model.ChatMessage + +interface SystemMessageInterface { + fun expandSystemMessage(chatMessage: ChatMessage) + fun collapseSystemMessages() +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt new file mode 100644 index 0000000..883642d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/SystemMessageViewHolder.kt @@ -0,0 +1,194 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.text.Spannable +import android.text.SpannableString +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.util.TypedValue +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.core.view.ViewCompat +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemSystemMessageBinding +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class SystemMessageViewHolder(itemView: View) : + MessageHolders + .IncomingTextMessageViewHolder(itemView) { + + private val binding: ItemSystemMessageBinding = ItemSystemMessageBinding.bind(itemView) + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @JvmField + @Inject + var appPreferences: AppPreferences? = null + + @JvmField + @Inject + var context: Context? = null + + @JvmField + @Inject + var dateUtils: DateUtils? = null + protected var background: ViewGroup + + lateinit var systemMessageInterface: SystemMessageInterface + + init { + sharedApplication!!.componentApplication.inject(this) + background = itemView.findViewById(R.id.container) + } + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + val user = currentUserProvider.currentUser.blockingGet() + val resources = itemView.resources + val pressedColor: Int = resources.getColor(R.color.bg_message_list_incoming_bubble) + val mentionColor: Int = resources.getColor(R.color.textColorMaxContrast) + val bubbleDrawable = DisplayUtils.getMessageSelector( + resources.getColor(R.color.transparent), + resources.getColor(R.color.transparent), + pressedColor, + R.drawable.shape_grouped_incoming_message + ) + ViewCompat.setBackground(background, bubbleDrawable) + binding.messageText.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + resources.getDimension(R.dimen.chat_system_message_text_size) + ) + var messageString: Spannable = SpannableString(message.text) + if (message.messageParameters != null && message.messageParameters!!.size > 0) { + for (key in message.messageParameters!!.keys) { + val individualMap: Map? = message.messageParameters!![key] + if (individualMap != null && individualMap.containsKey("name")) { + var searchText: String? = if ("user" == individualMap["type"] || + "guest" == individualMap["type"] || + "call" == individualMap["type"] + ) { + "@" + individualMap["name"] + } else { + individualMap["name"] + } + messageString = + DisplayUtils.searchAndColor( + messageString, + searchText!!, + mentionColor, + resources.getDimensionPixelSize(R.dimen.chat_system_message_text_size) + ) + if (individualMap["link"] != null) { + val displayName = individualMap["name"] ?: "" + val link = (user.baseUrl + individualMap["link"]) + val newStartIndex = messageString.indexOf(displayName) + if (newStartIndex != -1) { + val clickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri()) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context?.startActivity(browserIntent) + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.color = mentionColor + ds.isUnderlineText = false + } + } + + messageString.setSpan( + clickableSpan, + newStartIndex, + newStartIndex + displayName.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + } + } + + binding.systemMessageLayout.visibility = View.VISIBLE + binding.similarMessagesHint.visibility = View.GONE + if (message.expandableParent) { + processExpandableParent(message, messageString) + } else if (message.hiddenByCollapse) { + binding.systemMessageLayout.visibility = View.GONE + } else { + binding.expandCollapseIcon.visibility = View.GONE + binding.messageText.text = messageString + binding.expandCollapseIcon.setImageDrawable(null) + binding.systemMessageLayout.setOnClickListener(null) + } + + if (!message.expandableParent && message.lastItemOfExpandableGroup != 0) { + binding.systemMessageLayout.setOnClickListener { systemMessageInterface.collapseSystemMessages() } + binding.messageText.setOnClickListener { systemMessageInterface.collapseSystemMessages() } + } + + binding.messageTime.text = dateUtils!!.getLocalTimeStringFromTimestamp(message.timestamp) + itemView.setTag(R.string.replyable_message_view_tag, message.replyable) + } + } + + @SuppressLint("SetTextI18n") + private fun processExpandableParent(message: ChatMessage, messageString: Spannable) { + binding.expandCollapseIcon.visibility = View.VISIBLE + + if (!message.isExpanded) { + val similarMessages = sharedApplication!!.resources.getQuantityString( + R.plurals.see_similar_system_messages, + message.expandableChildrenAmount, + message.expandableChildrenAmount + ) + + binding.messageText.text = messageString + binding.messageText.movementMethod = LinkMovementMethod.getInstance() + binding.similarMessagesHint.visibility = View.VISIBLE + binding.similarMessagesHint.text = similarMessages + + binding.expandCollapseIcon.setImageDrawable( + ContextCompat.getDrawable(context!!, R.drawable.baseline_unfold_more_24) + ) + binding.systemMessageLayout.setOnClickListener { systemMessageInterface.expandSystemMessage(message) } + binding.messageText.setOnClickListener { systemMessageInterface.expandSystemMessage(message) } + } else { + binding.messageText.text = messageString + binding.similarMessagesHint.visibility = View.GONE + binding.similarMessagesHint.text = "" + + binding.expandCollapseIcon.setImageDrawable( + ContextCompat.getDrawable(context!!, R.drawable.baseline_unfold_less_24) + ) + binding.systemMessageLayout.setOnClickListener { systemMessageInterface.collapseSystemMessages() } + binding.messageText.setOnClickListener { systemMessageInterface.collapseSystemMessages() } + } + } + + fun assignSystemMessageInterface(systemMessageInterface: SystemMessageInterface) { + this.systemMessageInterface = systemMessageInterface + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java new file mode 100644 index 0000000..a51dde4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java @@ -0,0 +1,86 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages; + +import com.nextcloud.talk.chat.ChatActivity; +import com.stfalcon.chatkit.commons.ImageLoader; +import com.stfalcon.chatkit.commons.ViewHolder; +import com.stfalcon.chatkit.commons.models.IMessage; +import com.stfalcon.chatkit.messages.MessageHolders; +import com.stfalcon.chatkit.messages.MessagesListAdapter; + +import java.util.List; + +public class TalkMessagesListAdapter extends MessagesListAdapter { + private final ChatActivity chatActivity; + + public TalkMessagesListAdapter( + String senderId, + MessageHolders holders, + ImageLoader imageLoader, + ChatActivity chatActivity) { + super(senderId, holders, imageLoader); + this.chatActivity = chatActivity; + } + + public List getItems() { + return items; + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + + if (holder instanceof IncomingTextMessageViewHolder holderInstance) { + holderInstance.assignCommonMessageInterface(chatActivity); + } else if (holder instanceof OutcomingTextMessageViewHolder holderInstance) { + holderInstance.assignCommonMessageInterface(chatActivity); + holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation()); + + } else if (holder instanceof IncomingLocationMessageViewHolder holderInstance) { + holderInstance.assignCommonMessageInterface(chatActivity); + } else if (holder instanceof OutcomingLocationMessageViewHolder holderInstance) { + holderInstance.assignCommonMessageInterface(chatActivity); + holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation()); + + } else if (holder instanceof IncomingLinkPreviewMessageViewHolder holderInstance) { + holderInstance.assignCommonMessageInterface(chatActivity); + } else if (holder instanceof OutcomingLinkPreviewMessageViewHolder holderInstance) { + holderInstance.assignCommonMessageInterface(chatActivity); + holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation()); + + } else if (holder instanceof IncomingVoiceMessageViewHolder holderInstance) { + holderInstance.assignVoiceMessageInterface(chatActivity); + holderInstance.assignCommonMessageInterface(chatActivity); + } else if (holder instanceof OutcomingVoiceMessageViewHolder holderInstance) { + holderInstance.assignVoiceMessageInterface(chatActivity); + holderInstance.assignCommonMessageInterface(chatActivity); + holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation()); + + } else if (holder instanceof PreviewMessageViewHolder holderInstance) { + holderInstance.assignPreviewMessageInterface(chatActivity); + holderInstance.assignCommonMessageInterface(chatActivity); + + } else if (holder instanceof SystemMessageViewHolder holderInstance) { + holderInstance.assignSystemMessageInterface(chatActivity); + + } else if (holder instanceof IncomingDeckCardViewHolder holderInstance) { + holderInstance.assignCommonMessageInterface(chatActivity); + } else if (holder instanceof OutcomingDeckCardViewHolder holderInstance) { + holderInstance.assignCommonMessageInterface(chatActivity); + holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation()); + + } else if (holder instanceof IncomingPollMessageViewHolder holderInstance) { + holderInstance.assignCommonMessageInterface(chatActivity); + } else if (holder instanceof OutcomingPollMessageViewHolder holderInstance) { + holderInstance.assignCommonMessageInterface(chatActivity); + holderInstance.adjustIfNoteToSelf(chatActivity.getCurrentConversation()); + } + + super.onBindViewHolder(holder, position); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/Thread.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/Thread.kt new file mode 100644 index 0000000..f2f6820 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/Thread.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import android.view.View +import com.nextcloud.talk.R +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ItemThreadTitleBinding + +class Thread { + + fun showThreadPreview( + chatActivity: ChatActivity, + message: ChatMessage, + threadBinding: ItemThreadTitleBinding, + reactionsBinding: ReactionsInsideMessageBinding, + openThread: (message: ChatMessage) -> Unit + ) { + val isFirstMessageOfThreadInNormalChat = chatActivity.conversationThreadId == null && message.isThread + if (isFirstMessageOfThreadInNormalChat) { + threadBinding.threadTitleLayout.visibility = View.VISIBLE + + threadBinding.threadTitleLayout.findViewById(R.id.threadTitle).text = + message.threadTitle + + reactionsBinding.threadButton.visibility = View.VISIBLE + + reactionsBinding.threadButton.setContent { + ThreadButtonComposable( + message.threadReplies ?: 0, + onButtonClick = { openThread(message) } + ) + } + } else { + threadBinding.threadTitleLayout.visibility = View.GONE + reactionsBinding.threadButton.visibility = View.GONE + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/ThreadButton.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/ThreadButton.kt new file mode 100644 index 0000000..22481bd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/ThreadButton.kt @@ -0,0 +1,88 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.adapters.messages + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.R + +@Composable +fun ThreadButtonComposable(replyAmount: Int = 0, onButtonClick: () -> Unit = {}) { + val replyAmountText = if (replyAmount == 0) { + stringResource(R.string.thread_reply) + } else { + pluralStringResource( + R.plurals.thread_replies, + replyAmount, + replyAmount + ) + } + + OutlinedButton( + onClick = onButtonClick, + modifier = Modifier + .padding(8.dp) + .height(24.dp), + shape = RoundedCornerShape(9.dp), + border = BorderStroke(1.dp, colorResource(R.color.grey_600)), + contentPadding = PaddingValues(0.dp), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = Color.Transparent, + contentColor = colorResource(R.color.grey_600) + ) + ) { + Icon( + painter = painterResource(R.drawable.ic_reply), + contentDescription = stringResource(R.string.open_thread), + modifier = Modifier + .size(20.dp) + .padding(start = 5.dp, end = 2.dp), + tint = colorResource(R.color.grey_600) + ) + Text( + text = replyAmountText, + modifier = Modifier + .padding(end = 5.dp) + ) + } +} + +@Preview +@Composable +fun ThreadButtonPreviewMultipleReplies() { + ThreadButtonComposable(2) +} + +@Preview +@Composable +fun ThreadButtonPreviewOneReply() { + ThreadButtonComposable(1) +} + +@Preview +@Composable +fun ThreadButtonPreviewZeroReplies() { + ThreadButtonComposable(0) +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/UnreadNoticeMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/UnreadNoticeMessageViewHolder.java new file mode 100644 index 0000000..2628470 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/UnreadNoticeMessageViewHolder.java @@ -0,0 +1,37 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages; + +import android.view.View; + +import com.nextcloud.talk.chat.data.model.ChatMessage; +import com.stfalcon.chatkit.messages.MessageHolders; + +public class UnreadNoticeMessageViewHolder extends MessageHolders.SystemMessageViewHolder { + + public UnreadNoticeMessageViewHolder(View itemView) { + super(itemView); + } + + public UnreadNoticeMessageViewHolder(View itemView, Object payload) { + super(itemView, payload); + } + + @Override + public void viewDetached() { + } + + @Override + public void viewAttached() { + } + + @Override + public void viewRecycled() { + + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt new file mode 100644 index 0000000..34a9460 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.adapters.messages + +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.ui.PlaybackSpeed + +interface VoiceMessageInterface { + fun updateMediaPlayerProgressBySlider(message: ChatMessage, progress: Int) + fun registerMessageToObservePlaybackSpeedPreferences(userId: String, listener: (speed: PlaybackSpeed) -> Unit) +} diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java new file mode 100644 index 0000000..ca5d7af --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -0,0 +1,649 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.api; + +import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall; +import com.nextcloud.talk.models.json.capabilities.RoomCapabilitiesOverall; +import com.nextcloud.talk.models.json.chat.ChatOverall; +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage; +import com.nextcloud.talk.models.json.chat.ChatShareOverall; +import com.nextcloud.talk.models.json.chat.ChatShareOverviewOverall; +import com.nextcloud.talk.models.json.conversations.RoomOverall; +import com.nextcloud.talk.models.json.conversations.RoomsOverall; +import com.nextcloud.talk.models.json.generic.GenericOverall; +import com.nextcloud.talk.models.json.generic.Status; +import com.nextcloud.talk.models.json.hovercard.HoverCardOverall; +import com.nextcloud.talk.models.json.invitation.InvitationOverall; +import com.nextcloud.talk.models.json.mention.MentionOverall; +import com.nextcloud.talk.models.json.notifications.NotificationOverall; +import com.nextcloud.talk.models.json.opengraph.OpenGraphOverall; +import com.nextcloud.talk.models.json.participants.AddParticipantOverall; +import com.nextcloud.talk.models.json.participants.ParticipantsOverall; +import com.nextcloud.talk.models.json.participants.TalkBanOverall; +import com.nextcloud.talk.models.json.push.PushRegistrationOverall; +import com.nextcloud.talk.models.json.reactions.ReactionsOverall; +import com.nextcloud.talk.models.json.reminder.ReminderOverall; +import com.nextcloud.talk.models.json.search.ContactsByNumberOverall; +import com.nextcloud.talk.models.json.signaling.SignalingOverall; +import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall; +import com.nextcloud.talk.models.json.status.StatusOverall; +import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchOverall; +import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall; +import com.nextcloud.talk.models.json.userprofile.UserProfileOverall; +import com.nextcloud.talk.polls.repositories.model.PollOverall; +import com.nextcloud.talk.translate.repositories.model.LanguagesOverall; +import com.nextcloud.talk.translate.repositories.model.TranslationsOverall; + +import java.util.List; +import java.util.Map; + +import androidx.annotation.Nullable; +import io.reactivex.Observable; +import kotlin.Unit; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.Field; +import retrofit2.http.FieldMap; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.HEAD; +import retrofit2.http.Header; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Part; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; +import retrofit2.http.Url; + +public interface NcApi { + + /* + QueryMap items are as follows: + - "format" : "json" + - "search" : "" + - "perPage" : "200" + - "itemType" : "call" + + Server URL is: baseUrl + ocsApiVersion + /apps/files_sharing/api/v1/sharees + + or if we're on 14 and up: + + baseUrl + ocsApiVersion + "/core/autocomplete/get"); + + */ + @GET + Observable getContactsWithSearchParam(@Header("Authorization") String authorization, + @Url String url, + @Nullable @Query("shareTypes[]") List listOfShareTypes, + @QueryMap Map options); + + + /* + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room + */ + @GET + Observable getRooms(@Header("Authorization") String authorization, + @Url String url, + @Nullable @Query("includeStatus") Boolean includeStatus); + + /* + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken + */ + @GET + Observable getRoom(@Header("Authorization") String authorization, @Url String url); + + /* + QueryMap items are as follows: + - "roomType" : "" + - "invite" : "" + + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room + */ + + @POST + Observable createRoom(@Header("Authorization") String authorization, + @Url String url, + @QueryMap Map options); + + /* + QueryMap items are as follows: + - "newParticipant" : "user" + + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken/participants + */ + @POST + Observable addParticipant(@Header("Authorization") String authorization, + @Url String url, + @QueryMap Map options); + + @POST + Observable resendParticipantInvitations(@Header("Authorization") String authorization, + @Url String url); + + // also used for removing a guest from a conversation + @Deprecated + @DELETE + Observable removeParticipantFromConversation(@Header("Authorization") String authorization, + @Url String url, + @Query("participant") String participantId); + + @DELETE + Observable removeAttendeeFromConversation(@Header("Authorization") String authorization, + @Url String url, + @Query("attendeeId") Long attendeeId); + + @Deprecated + @POST + Observable promoteUserToModerator(@Header("Authorization") String authorization, + @Url String url, + @Query("participant") String participantId); + + @Deprecated + @DELETE + Observable demoteModeratorToUser(@Header("Authorization") String authorization, + @Url String url, + @Query("participant") String participantId); + + @POST + Observable promoteAttendeeToModerator(@Header("Authorization") String authorization, + @Url String url, + @Query("attendeeId") Long attendeeId); + + @DELETE + Observable demoteAttendeeFromModerator(@Header("Authorization") String authorization, + @Url String url, + @Query("attendeeId") Long attendeeId); + + /* + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken/participants/self + */ + + @DELETE + Observable removeSelfFromRoom(@Header("Authorization") String authorization, @Url String url); + + @DELETE + Observable deleteRoom(@Header("Authorization") String authorization, @Url String url); + + /* + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken + */ + @GET + Observable getPeersForCall(@Header("Authorization") String authorization, @Url String url); + + @GET + Observable getPeersForCall(@Header("Authorization") String authorization, + @Url String url, + @QueryMap Map fields); + + @FormUrlEncoded + @POST + Observable joinRoom(@Nullable @Header("Authorization") String authorization, + @Url String url, + @Nullable @Field("password") String password); + + @DELETE + Observable leaveRoom(@Nullable @Header("Authorization") String authorization, @Url String url); + + /* + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken + */ + + @FormUrlEncoded + @POST + Observable joinCall(@Nullable @Header("Authorization") String authorization, + @Url String url, + @Field("flags") Integer inCall, + @Field("silent") Boolean callWithoutNotification, + @Nullable @Field("recordingConsent") Boolean recordingConsent); + + /* + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /call/callToken + */ + @DELETE + Observable leaveCall(@Nullable @Header("Authorization") String authorization, @Url String url, + @Nullable @Query("all") Boolean all); + + @GET + Observable getSignalingSettings(@Nullable @Header("Authorization") String authorization, + @Url String url); + + /* + QueryMap items are as follows: + - "messages" : "message" + + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /signaling + */ + @FormUrlEncoded + @POST + Observable sendSignalingMessages(@Nullable @Header("Authorization") String authorization, + @Url String url, + @Field("messages") String messages); + + /* + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /signaling + */ + @GET + Observable pullSignalingMessages(@Nullable @Header("Authorization") String authorization, + @Url String url); + + /* + QueryMap items are as follows: + - "format" : "json" + + Server URL is: baseUrl + ocsApiVersion + "/cloud/user" + */ + + @GET + Observable getUserProfile(@Header("Authorization") String authorization, @Url String url); + + + @GET + Observable getUserData(@Header("Authorization") String authorization, @Url String url); + + @DELETE + Observable revertStatus(@Header("Authentication") String authorization, @Url String url); + + @FormUrlEncoded + @PUT + Observable setUserData(@Header("Authorization") String authorization, + @Url String url, + @Field("key") String key, + @Field("value") String value); + + + /* + Server URL is: baseUrl + /status.php + */ + @GET + Observable getServerStatus(@Url String url); + + + /* + QueryMap items are as follows: + - "format" : "json" + - "pushTokenHash" : "" + - "devicePublicKey" : "" + - "proxyServer" : "" + + Server URL is: baseUrl + ocsApiVersion + "/apps/notifications/api/v2/push + */ + + @POST + Observable registerDeviceForNotificationsWithNextcloud( + @Header("Authorization") String authorization, + @Url String url, + @QueryMap Map options); + + @DELETE + Observable unregisterDeviceForNotificationsWithNextcloud( + @Header("Authorization") String authorization, + @Url String url); + + @FormUrlEncoded + @POST + Observable registerDeviceForNotificationsWithPushProxy(@Url String url, + @FieldMap Map fields); + + + /* + QueryMap items are as follows: + - "deviceIdentifier": "{{deviceIdentifier}}", + - "deviceIdentifierSignature": "{{signature}}", + - "userPublicKey": "{{userPublicKey}}" + */ + @DELETE + Observable unregisterDeviceForNotificationsWithProxy(@Url String url, + @QueryMap Map fields); + + @FormUrlEncoded + @PUT + Observable> setPassword2(@Header("Authorization") String authorization, + @Url String url, + @Field("password") String password); + + @GET + Observable getCapabilities(@Header("Authorization") String authorization, @Url String url); + + @GET + Observable getCapabilities(@Url String url); + + @GET + Observable getRoomCapabilities(@Header("Authorization") String authorization, + @Url String url); + + /* + QueryMap items are as follows: + - "lookIntoFuture": int (0 or 1), + - "limit" : int, range 100-200, + - "timeout": used with look into future, 30 default, 60 at most + - "lastKnownMessageId", int, use one from X-Chat-Last-Given + */ + @GET + Observable> pullChatMessages(@Header("Authorization") String authorization, + @Url String url, + @QueryMap Map fields); + + /* + Fieldmap items are as follows: + - "message": , + - "actorDisplayName" + */ + + @FormUrlEncoded + @POST + Observable sendChatMessage(@Header("Authorization") String authorization, + @Url String url, + @Field("message") CharSequence message, + @Field("actorDisplayName") String actorDisplayName, + @Field("replyTo") Integer replyTo, + @Field("silent") Boolean sendWithoutNotification, + @Field("referenceId") String referenceId + ); + + @GET + Observable> getSharedItems( + @Header("Authorization") String authorization, + @Url String url, + @Query("objectType") String objectType, + @Nullable @Query("lastKnownMessageId") Integer lastKnownMessageId, + @Nullable @Query("limit") Integer limit); + + @GET + Observable> getSharedItemsOverview(@Header("Authorization") String authorization, + @Url String url, + @Nullable @Query("limit") Integer limit); + + + @GET + Observable getMentionAutocompleteSuggestions(@Header("Authorization") String authorization, + @Url String url, + @Query("search") String query, + @Nullable @Query("limit") Integer limit, + @QueryMap Map fields); + + @GET + Observable getNcNotification(@Header("Authorization") String authorization, + @Url String url); + + @FormUrlEncoded + @POST + Observable setNotificationLevel(@Header("Authorization") String authorization, + @Url String url, + @Field("level") int level); + + @FormUrlEncoded + @PUT + Observable setConversationReadOnly(@Header("Authorization") String authorization, + @Url String url, + @Field("state") int state); + + @FormUrlEncoded + @POST + Observable createRemoteShare(@Nullable @Header("Authorization") String authorization, + @Url String url, + @Field("path") String remotePath, + @Field("shareWith") String roomToken, + @Field("shareType") String shareType, + @Field("talkMetaData") String talkMetaData); + + @FormUrlEncoded + @PUT + Observable setLobbyForConversation(@Header("Authorization") String authorization, + @Url String url, + @Field("state") Integer state, + @Field("timer") Long timer); + + @POST + Observable searchContactsByPhoneNumber(@Header("Authorization") String authorization, + @Url String url, + @Body RequestBody search); + + @PUT + Observable> uploadFile(@Header("Authorization") String authorization, + @Url String url, + @Body RequestBody body); + + @HEAD + Observable> checkIfFileExists(@Header("Authorization") String authorization, + @Url String url); + + @GET + Call downloadFile(@Header("Authorization") String authorization, + @Url String url); + + @DELETE + Observable deleteChatMessage(@Header("Authorization") String authorization, + @Url String url); + + @DELETE + Observable deleteAvatar(@Header("Authorization") String authorization, @Url String url); + + @DELETE + Observable deleteConversationAvatar(@Header("Authorization") String authorization, @Url String url); + + + @Multipart + @POST + Observable uploadAvatar(@Header("Authorization") String authorization, + @Url String url, + @Part MultipartBody.Part attachment); + + @Multipart + @POST + Observable uploadConversationAvatar(@Header("Authorization") String authorization, + @Url String url, + @Part MultipartBody.Part attachment); + + @GET + Observable getEditableUserProfileFields(@Header("Authorization") String authorization, + @Url String url); + + @GET + Call downloadResizedImage(@Header("Authorization") String authorization, + @Url String url); + + @FormUrlEncoded + @POST + Observable sendLocation(@Header("Authorization") String authorization, + @Url String url, + @Field("objectType") String objectType, + @Field("objectId") String objectId, + @Field("metaData") String metaData); + + @FormUrlEncoded + @POST + Observable notificationCalls(@Header("Authorization") String authorization, + @Url String url, + @Field("level") Integer level); + + @DELETE + Observable clearChatHistory(@Header("Authorization") String authorization, @Url String url); + + @GET + Observable hoverCard(@Header("Authorization") String authorization, @Url String url); + + // Url is: /api/{apiVersion}/chat/{token}/read + @FormUrlEncoded + @POST + Observable setChatReadMarker(@Header("Authorization") String authorization, + @Url String url, + @Nullable @Field("lastReadMessage") Integer lastReadMessage); + + // Url is: /api/{apiVersion}/chat/{token}/read + @DELETE + Observable markRoomAsUnread(@Header("Authorization") String authorization, @Url String url); + + /* + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /listed-room + */ + @GET + Observable getOpenConversations(@Header("Authorization") String authorization, @Url String url, + @Query("searchTerm") String searchTerm); + + @GET + Observable status(@Header("Authorization") String authorization, @Url String url); + + @GET + Observable backupStatus(@Header("Authorization") String authorization, @Url String url); + + @GET + Observable getPredefinedStatuses(@Header("Authorization") String authorization, @Url String url); + + @DELETE + Observable statusDeleteMessage(@Header("Authorization") String authorization, @Url String url); + + + @FormUrlEncoded + @PUT + Observable setPredefinedStatusMessage(@Header("Authorization") String authorization, + @Url String url, + @Field("messageId") String selectedPredefinedMessageId, + @Field("clearAt") Long clearAt); + + @FormUrlEncoded + @PUT + Observable setCustomStatusMessage(@Header("Authorization") String authorization, + @Url String url, + @Field("statusIcon") String statusIcon, + @Field("message") String message, + @Field("clearAt") Long clearAt); + + @FormUrlEncoded + @PUT + Observable setStatusType(@Header("Authorization") String authorization, + @Url String url, + @Field("statusType") String statusType); + + + @POST + Observable sendReaction(@Header("Authorization") String authorization, + @Url String url, + @Query("reaction") String reaction); + + @DELETE + Observable deleteReaction(@Header("Authorization") String authorization, + @Url String url, + @Query("reaction") String reaction); + + @GET + Observable getReactions(@Header("Authorization") String authorization, + @Url String url, + @Query("reaction") String reaction); + + @GET + Observable performUnifiedSearch(@Header("Authorization") String authorization, + @Url String url, + @Query("term") String term, + @Query("from") String fromUrl, + @Query("limit") Integer limit, + @Query("cursor") Integer cursor); + + @GET + Observable getPoll(@Header("Authorization") String authorization, + @Url String url); + + @FormUrlEncoded + @POST + Observable createPoll(@Header("Authorization") String authorization, + @Url String url, + @Query("question") String question, + @Field("options[]") List options, + @Query("resultMode") Integer resultMode, + @Query("maxVotes") Integer maxVotes); + + @FormUrlEncoded + @POST + Observable votePoll(@Header("Authorization") String authorization, + @Url String url, + @Field("optionIds[]") List optionIds); + + @DELETE + Observable closePoll(@Header("Authorization") String authorization, + @Url String url); + + @GET + Observable getOpenGraph(@Header("Authorization") String authorization, + @Url String url, + @Query("reference") String urlToFindPreviewFor); + + @FormUrlEncoded + @POST + Observable startRecording(@Header("Authorization") String authorization, + @Url String url, + @Field("status") Integer status); + + @DELETE + Observable stopRecording(@Header("Authorization") String authorization, + @Url String url); + + @POST + Observable requestAssistance(@Header("Authorization") String authorization, + @Url String url); + + @DELETE + Observable withdrawRequestAssistance(@Header("Authorization") String authorization, + @Url String url); + + @POST + Observable sendCommonPostRequest(@Header("Authorization") String authorization, @Url String url); + + @DELETE + Observable sendCommonDeleteRequest(@Header("Authorization") String authorization, @Url String url); + + + @POST + Observable translateMessage(@Header("Authorization") String authorization, + @Url String url, + @Query("text") String text, + @Query("toLanguage") String toLanguage, + @Nullable @Query("fromLanguage") String fromLanguage); + + @GET + Observable getLanguages(@Header("Authorization") String authorization, + @Url String url); + + @GET + Observable getReminder(@Header("Authorization") String authorization, + @Url String url); + + @DELETE + Observable deleteReminder(@Header("Authorization") String authorization, + @Url String url); + + @FormUrlEncoded + @POST + Observable setReminder(@Header("Authorization") String authorization, + @Url String url, + @Field("timestamp") int timestamp); + + @FormUrlEncoded + @PUT + Observable setRecordingConsent(@Header("Authorization") String authorization, + @Url String url, + @Field("recordingConsent") int recordingConsent); + + @GET + Observable getInvitations(@Header("Authorization") String authorization, + @Url String url); + + @POST + Observable acceptInvitation(@Header("Authorization") String authorization, + @Url String url); + + @DELETE + Observable rejectInvitation(@Header("Authorization") String authorization, + @Url String url); +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt new file mode 100644 index 0000000..4211aa2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt @@ -0,0 +1,309 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.api + +import com.nextcloud.talk.conversationinfo.CreateRoomRequest +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.chat.ChatOverall +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.participants.AddParticipantOverall +import com.nextcloud.talk.models.json.participants.TalkBan +import com.nextcloud.talk.models.json.participants.TalkBanOverall +import com.nextcloud.talk.models.json.profile.ProfileOverall +import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall +import com.nextcloud.talk.models.json.threads.ThreadOverall +import com.nextcloud.talk.models.json.threads.ThreadsOverall +import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Part +import retrofit2.http.Query +import retrofit2.http.QueryMap +import retrofit2.http.Url + +@Suppress("TooManyFunctions") +interface NcApiCoroutines { + @GET + @JvmSuppressWildcards + suspend fun getContactsWithSearchParam( + @Header("Authorization") authorization: String?, + @Url url: String?, + @Query("shareTypes[]") listOfShareTypes: List?, + @QueryMap options: Map? + ): AutocompleteOverall + + /* + QueryMap items are as follows: + - "roomType" : "" + - "invite" : "" + + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room + */ + @POST + suspend fun createRoom( + @Header("Authorization") authorization: String?, + @Url url: String?, + @QueryMap options: Map? + ): RoomOverall + + @POST + suspend fun createRoomWithBody( + @Header("Authorization") authorization: String?, + @Url url: String?, + @Body roomRequest: CreateRoomRequest + ): RoomOverall + + /* + QueryMap items are as follows: + - "roomName" : "newName" + + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken + */ + @FormUrlEncoded + @PUT + suspend fun renameRoom( + @Header("Authorization") authorization: String?, + @Url url: String, + @Field("roomName") roomName: String? + ): GenericOverall + + @FormUrlEncoded + @PUT + suspend fun openConversation( + @Header("Authorization") authorization: String?, + @Url url: String, + @Field("scope") scope: Int + ): GenericOverall + + @FormUrlEncoded + @PUT + suspend fun setConversationDescription( + @Header("Authorization") authorization: String?, + @Url url: String, + @Field("description") description: String? + ): GenericOverall + + @POST + suspend fun addParticipant( + @Header("Authorization") authorization: String?, + @Url url: String?, + @QueryMap options: Map? + ): AddParticipantOverall + + @POST + suspend fun makeRoomPublic(@Header("Authorization") authorization: String, @Url url: String): GenericOverall + + @DELETE + suspend fun makeRoomPrivate(@Header("Authorization") authorization: String, @Url url: String): GenericOverall + + @FormUrlEncoded + @PUT + suspend fun setPassword( + @Header("Authorization") authorization: String?, + @Url url: String?, + @Field("password") password: String? + ): GenericOverall + + @Multipart + @POST + suspend fun uploadConversationAvatar( + @Header("Authorization") authorization: String, + @Url url: String, + @Part attachment: MultipartBody.Part + ): RoomOverall + + @DELETE + suspend fun deleteConversationAvatar(@Header("Authorization") authorization: String, @Url url: String): RoomOverall + + @POST + suspend fun archiveConversation(@Header("Authorization") authorization: String, @Url url: String): GenericOverall + + @DELETE + suspend fun unarchiveConversation(@Header("Authorization") authorization: String, @Url url: String): GenericOverall + + @Suppress("LongParameterList") + @FormUrlEncoded + @POST + suspend fun sendChatMessage( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("message") message: String, + @Field("actorDisplayName") actorDisplayName: String, + @Field("replyTo") replyTo: Int, + @Field("silent") sendWithoutNotification: Boolean, + @Field("referenceId") referenceId: String, + @Field("threadTitle") threadTitle: String? + ): ChatOverallSingleMessage + + @FormUrlEncoded + @PUT + suspend fun editChatMessage( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("message") message: String + ): ChatOverallSingleMessage + + @FormUrlEncoded + @POST + suspend fun banActor( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("actorType") actorType: String, + @Field("actorId") actorId: String, + @Field("internalNote") internalNote: String + ): TalkBan + + @GET + suspend fun listBans(@Header("Authorization") authorization: String, @Url url: String): TalkBanOverall + + @DELETE + suspend fun unbanActor(@Header("Authorization") authorization: String, @Url url: String): GenericOverall + + @POST + suspend fun addConversationToFavorites( + @Header("Authorization") authorization: String, + @Url url: String + ): GenericOverall + + @POST + suspend fun markConversationAsImportant( + @Header("Authorization") authorization: String, + @Url url: String + ): GenericOverall + + @DELETE + suspend fun markConversationAsUnimportant( + @Header("Authorization") authorization: String, + @Url url: String + ): GenericOverall + + @DELETE + suspend fun removeConversationFromFavorites( + @Header("Authorization") authorization: String, + @Url url: String + ): GenericOverall + + @POST + suspend fun markConversationAsSensitive( + @Header("Authorization") authorization: String, + @Url url: String + ): GenericOverall + + @DELETE + suspend fun markConversationAsInsensitive( + @Header("Authorization") authorization: String, + @Url url: String + ): GenericOverall + + @FormUrlEncoded + @POST + suspend fun notificationCalls( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("level") level: Int + ): GenericOverall + + @POST + suspend fun setReadStatusPrivacy( + @Header("Authorization") authorization: String, + @Url url: String, + @Body body: RequestBody + ): GenericOverall + + @POST + suspend fun setTypingStatusPrivacy( + @Header("Authorization") authorization: String, + @Url url: String, + @Body body: RequestBody + ): GenericOverall + + @DELETE + suspend fun clearChatHistory(@Header("Authorization") authorization: String, @Url url: String): GenericOverall + + @FormUrlEncoded + @PUT + suspend fun setConversationReadOnly( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("state") state: Int + ): GenericOverall + + @FormUrlEncoded + @POST + suspend fun setNotificationLevel( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("level") level: Int + ): GenericOverall + + @FormUrlEncoded + @POST + suspend fun setMessageExpiration( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("seconds") seconds: Int + ): GenericOverall + + @GET + suspend fun getOutOfOfficeStatusForUser( + @Header("Authorization") authorization: String, + @Url url: String + ): UserAbsenceOverall + + @POST + suspend fun testPushNotifications( + @Header("Authorization") authorization: String, + @Url url: String + ): TestNotificationOverall + + @GET + suspend fun getContextOfChatMessage( + @Header("Authorization") authorization: String, + @Url url: String, + @Query("limit") limit: Int + ): ChatOverall + + @GET + suspend fun getNoteToSelfRoom(@Header("Authorization") authorization: String, @Url url: String): RoomOverall + + @GET + suspend fun getProfile(@Header("Authorization") authorization: String, @Url url: String): ProfileOverall + + @DELETE + suspend fun unbindRoom(@Header("Authorization") authorization: String, @Url url: String): GenericOverall + + @GET + suspend fun getThreads( + @Header("Authorization") authorization: String, + @Url url: String, + @Query("limit") limit: Int? + ): ThreadsOverall + + @GET + suspend fun getThread(@Header("Authorization") authorization: String, @Url url: String): ThreadOverall + + @FormUrlEncoded + @POST + suspend fun setThreadNotificationLevel( + @Header("Authorization") authorization: String, + @Url url: String, + @Field("level") level: Int + ): ThreadOverall +} diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt new file mode 100644 index 0000000..6ab3c70 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt @@ -0,0 +1,252 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.application + +import android.content.Context +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES.P +import android.util.Log +import androidx.appcompat.app.AppCompatDelegate +import androidx.emoji2.bundled.BundledEmojiCompatConfig +import androidx.emoji2.text.EmojiCompat +import androidx.lifecycle.LifecycleObserver +import androidx.multidex.MultiDex +import androidx.multidex.MultiDexApplication +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import autodagger.AutoComponent +import autodagger.AutoInjector +import coil.Coil +import coil.ImageLoader +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.decode.SvgDecoder +import coil.memory.MemoryCache +import coil.util.DebugLogger +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.dagger.modules.BusModule +import com.nextcloud.talk.dagger.modules.ContextModule +import com.nextcloud.talk.dagger.modules.DaosModule +import com.nextcloud.talk.dagger.modules.DatabaseModule +import com.nextcloud.talk.dagger.modules.ManagerModule +import com.nextcloud.talk.dagger.modules.RepositoryModule +import com.nextcloud.talk.dagger.modules.RestModule +import com.nextcloud.talk.dagger.modules.UtilsModule +import com.nextcloud.talk.dagger.modules.ViewModelModule +import com.nextcloud.talk.filebrowser.webdav.DavUtils +import com.nextcloud.talk.jobs.AccountRemovalWorker +import com.nextcloud.talk.jobs.CapabilitiesWorker +import com.nextcloud.talk.jobs.SignalingSettingsWorker +import com.nextcloud.talk.jobs.WebsocketConnectionsWorker +import com.nextcloud.talk.ui.theme.ThemeModule +import com.nextcloud.talk.utils.ClosedInterfaceImpl +import com.nextcloud.talk.utils.DeviceUtils +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.database.arbitrarystorage.ArbitraryStorageModule +import com.nextcloud.talk.utils.database.user.UserModule +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.vanniktech.emoji.EmojiManager +import com.vanniktech.emoji.google.GoogleEmojiProvider +import de.cotech.hw.SecurityKeyManager +import de.cotech.hw.SecurityKeyManagerConfig +import okhttp3.OkHttpClient +import org.conscrypt.Conscrypt +import org.webrtc.PeerConnectionFactory +import java.security.Security +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +@AutoComponent( + modules = [ + BusModule::class, + ContextModule::class, + DatabaseModule::class, + RestModule::class, + UserModule::class, + ArbitraryStorageModule::class, + ViewModelModule::class, + RepositoryModule::class, + UtilsModule::class, + ThemeModule::class, + ManagerModule::class, + DaosModule::class + ] +) +@Singleton +@AutoInjector(NextcloudTalkApplication::class) +class NextcloudTalkApplication : + MultiDexApplication(), + LifecycleObserver { + //region Fields (components) + lateinit var componentApplication: NextcloudTalkApplicationComponent + private set + //endregion + + //region Getters + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var okHttpClient: OkHttpClient + //endregion + + //region private methods + private fun initializeWebRtc() { + try { + PeerConnectionFactory.initialize( + PeerConnectionFactory.InitializationOptions.builder(this) + .createInitializationOptions() + ) + } catch (e: UnsatisfiedLinkError) { + Log.w(TAG, e) + } + } + + //endregion + + //region Overridden methods + override fun onCreate() { + Log.d(TAG, "onCreate") + sharedApplication = this + + val securityKeyManager = SecurityKeyManager.getInstance() + val securityKeyConfig = SecurityKeyManagerConfig.Builder() + .setEnableDebugLogging(BuildConfig.DEBUG) + .build() + securityKeyManager.init(this, securityKeyConfig) + + initializeWebRtc() + buildComponent() + DavUtils.registerCustomFactories() + + Security.insertProviderAt(Conscrypt.newProvider(), 1) + + componentApplication.inject(this) + + Coil.setImageLoader(buildDefaultImageLoader()) + setAppTheme(appPreferences.theme) + super.onCreate() + + ClosedInterfaceImpl().providerInstallerInstallIfNeededAsync() + DeviceUtils.ignoreSpecialBatteryFeatures() + + initWorkers() + + val config = BundledEmojiCompatConfig(this) + config.setReplaceAll(true) + val emojiCompat = EmojiCompat.init(config) + + EmojiManager.install(GoogleEmojiProvider()) + + NotificationUtils.registerNotificationChannels(applicationContext, appPreferences) + } + + private fun initWorkers() { + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + val capabilitiesUpdateWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java).build() + val signalingSettingsWork = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java).build() + val websocketConnectionsWorker = OneTimeWorkRequest.Builder(WebsocketConnectionsWorker::class.java).build() + + WorkManager.getInstance(applicationContext) + .beginWith(accountRemovalWork) + .then(capabilitiesUpdateWork) + .then(signalingSettingsWork) + .then(websocketConnectionsWorker) + .enqueue() + + val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder( + CapabilitiesWorker::class.java, + HALF_DAY, + TimeUnit.HOURS + ).build() + WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork( + "DailyCapabilitiesUpdateWork", + ExistingPeriodicWorkPolicy.REPLACE, + periodicCapabilitiesUpdateWork + ) + } + + override fun onTerminate() { + super.onTerminate() + sharedApplication = null + } + //endregion + + //region Protected methods + protected fun buildComponent() { + componentApplication = DaggerNextcloudTalkApplicationComponent.builder() + .busModule(BusModule()) + .contextModule(ContextModule(applicationContext)) + .databaseModule(DatabaseModule()) + .restModule(RestModule(applicationContext)) + .arbitraryStorageModule(ArbitraryStorageModule()) + .build() + } + + override fun attachBaseContext(base: Context) { + super.attachBaseContext(base) + MultiDex.install(this) + } + + private fun buildDefaultImageLoader(): ImageLoader { + val imageLoaderBuilder = ImageLoader.Builder(applicationContext) + .memoryCache { + // Use 50% of the application's available memory. + MemoryCache.Builder(applicationContext).maxSizePercent(FIFTY_PERCENT).build() + } + .crossfade(true) // Show a short crossfade when loading images from network or disk into an ImageView. + .components { + if (SDK_INT >= P) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + add(SvgDecoder.Factory()) + } + + if (BuildConfig.DEBUG) { + imageLoaderBuilder.logger(DebugLogger()) + } + + imageLoaderBuilder.okHttpClient(okHttpClient) + + return imageLoaderBuilder.build() + } + + companion object { + private val TAG = NextcloudTalkApplication::class.java.simpleName + const val FIFTY_PERCENT = 0.5 + const val HALF_DAY: Long = 12 + const val CIPHER_V4_MIGRATION: Int = 7 + //region Singleton + //endregion + + var sharedApplication: NextcloudTalkApplication? = null + protected set + //endregion + + //region Setters + fun setAppTheme(theme: String) { + when (theme) { + "night_no" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + "night_yes" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + "battery_saver" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY) + else -> + // will be "follow_system" only for now + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + } + } + } + //endregion +} diff --git a/app/src/main/java/com/nextcloud/talk/arbitrarystorage/ArbitraryStorageManager.kt b/app/src/main/java/com/nextcloud/talk/arbitrarystorage/ArbitraryStorageManager.kt new file mode 100644 index 0000000..e5ed7bd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/arbitrarystorage/ArbitraryStorageManager.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.arbitrarystorage + +import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository +import com.nextcloud.talk.data.storage.model.ArbitraryStorage +import io.reactivex.Maybe + +class ArbitraryStorageManager(private val arbitraryStoragesRepository: ArbitraryStoragesRepository) { + fun storeStorageSetting(accountIdentifier: Long, key: String, value: String?, objectString: String?) { + arbitraryStoragesRepository.saveArbitraryStorage(ArbitraryStorage(accountIdentifier, key, objectString, value)) + } + + fun getStorageSetting(accountIdentifier: Long, key: String, objectString: String): Maybe = + arbitraryStoragesRepository.getStorageSetting(accountIdentifier, key, objectString) + + fun deleteAllEntriesForAccountIdentifier(accountIdentifier: Long): Int = + arbitraryStoragesRepository.deleteArbitraryStorage(accountIdentifier) +} diff --git a/app/src/main/java/com/nextcloud/talk/bottomsheet/items/BasicListItemWithImage.kt b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/BasicListItemWithImage.kt new file mode 100644 index 0000000..429b823 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/BasicListItemWithImage.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.bottomsheet.items + +import android.widget.ImageView +import androidx.annotation.DrawableRes + +interface ListItemWithImage { + val title: String + fun populateIcon(imageView: ImageView) +} + +data class BasicListItemWithImage(@DrawableRes val iconRes: Int, override val title: String) : ListItemWithImage { + + override fun populateIcon(imageView: ImageView) { + imageView.setImageResource(iconRes) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/bottomsheet/items/BottomSheets.kt b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/BottomSheets.kt new file mode 100644 index 0000000..a9724f8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/BottomSheets.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.bottomsheet.items + +import androidx.annotation.CheckResult +import androidx.recyclerview.widget.LinearLayoutManager +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.internal.list.DialogAdapter +import com.afollestad.materialdialogs.list.customListAdapter +import com.afollestad.materialdialogs.list.getListAdapter + +typealias ListItemListener = + ((dialog: MaterialDialog, index: Int, item: IT) -> Unit)? + +@CheckResult +fun MaterialDialog.listItemsWithImage( + items: List, + disabledIndices: IntArray? = null, + waitForPositiveButton: Boolean = true, + selection: ListItemListener = null +): MaterialDialog { + if (getListAdapter() != null) { + return updateListItemsWithImage( + items = items, + disabledIndices = disabledIndices + ) + } + + val layoutManager = LinearLayoutManager(windowContext) + return customListAdapter( + adapter = ListIconDialogAdapter( + dialog = this, + items = items, + disabledItems = disabledIndices, + waitForPositiveButton = waitForPositiveButton, + selection = selection + ), + layoutManager = layoutManager + ) +} + +fun MaterialDialog.updateListItemsWithImage( + items: List, + disabledIndices: IntArray? = null +): MaterialDialog { + val adapter = getListAdapter() + check(adapter != null) { + "updateGridItems(...) can't be used before you've created a bottom sheet grid dialog." + } + if (adapter is DialogAdapter<*, *>) { + @Suppress("UNCHECKED_CAST") + (adapter as DialogAdapter).replaceItems(items) + + if (disabledIndices != null) { + adapter.disableItems(disabledIndices) + } + } + return this +} diff --git a/app/src/main/java/com/nextcloud/talk/bottomsheet/items/ListIconDialogAdapter.kt b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/ListIconDialogAdapter.kt new file mode 100644 index 0000000..233b294 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/bottomsheet/items/ListIconDialogAdapter.kt @@ -0,0 +1,131 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.bottomsheet.items + +import android.annotation.SuppressLint +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.WhichButton +import com.afollestad.materialdialogs.actions.hasActionButton +import com.afollestad.materialdialogs.actions.hasActionButtons +import com.afollestad.materialdialogs.internal.list.DialogAdapter +import com.afollestad.materialdialogs.internal.rtl.RtlTextView +import com.afollestad.materialdialogs.utils.MDUtil.inflate +import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor +import com.nextcloud.talk.R + +private const val KEY_ACTIVATED_INDEX = "activated_index" + +internal class ListItemViewHolder(itemView: View, private val adapter: ListIconDialogAdapter<*>) : + RecyclerView.ViewHolder(itemView), + View.OnClickListener { + init { + itemView.setOnClickListener(this) + } + + val iconView: ImageView = itemView.findViewById(R.id.icon) + val titleView: RtlTextView = itemView.findViewById(R.id.title) + + override fun onClick(view: View) = adapter.itemClicked(adapterPosition) +} + +internal class ListIconDialogAdapter( + private var dialog: MaterialDialog, + private var items: List, + disabledItems: IntArray?, + private var waitForPositiveButton: Boolean, + private var selection: ListItemListener +) : RecyclerView.Adapter(), + DialogAdapter> { + + private var disabledIndices: IntArray = disabledItems ?: IntArray(0) + + fun itemClicked(index: Int) { + if (waitForPositiveButton && dialog.hasActionButton(WhichButton.POSITIVE)) { + // Wait for positive action button, mark clicked item as activated so that we can call the + // selection listener when the button is pressed. + val lastActivated = dialog.config[KEY_ACTIVATED_INDEX] as? Int + dialog.config[KEY_ACTIVATED_INDEX] = index + if (lastActivated != null) { + notifyItemChanged(lastActivated) + } + notifyItemChanged(index) + } else { + // Don't wait for action buttons, call listener and dismiss if auto dismiss is applicable + this.selection?.invoke(dialog, index, this.items[index]) + if (dialog.autoDismissEnabled && !dialog.hasActionButtons()) { + dialog.dismiss() + } + } + } + + @SuppressLint("RestrictedApi") + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemViewHolder { + val listItemView: View = parent.inflate(dialog.windowContext, R.layout.menu_item_sheet) + val viewHolder = ListItemViewHolder( + itemView = listItemView, + adapter = this + ) + viewHolder.titleView.maybeSetTextColor(dialog.windowContext, R.attr.md_color_content) + return viewHolder + } + + override fun getItemCount() = items.size + + override fun onBindViewHolder(holder: ListItemViewHolder, position: Int) { + holder.itemView.isEnabled = !disabledIndices.contains(position) + val currentItem = items[position] + + holder.titleView.text = currentItem.title + currentItem.populateIcon(holder.iconView) + + val activatedIndex = dialog.config[KEY_ACTIVATED_INDEX] as? Int + holder.itemView.isActivated = activatedIndex != null && activatedIndex == position + + if (dialog.bodyFont != null) { + holder.titleView.typeface = dialog.bodyFont + } + } + + override fun positiveButtonClicked() { + val activatedIndex = dialog.config[KEY_ACTIVATED_INDEX] as? Int + if (activatedIndex != null) { + selection?.invoke(dialog, activatedIndex, items[activatedIndex]) + dialog.config.remove(KEY_ACTIVATED_INDEX) + } + } + + override fun replaceItems(items: List, listener: ListItemListener) { + this.items = items + if (listener != null) { + this.selection = listener + } + this.notifyDataSetChanged() + } + + override fun disableItems(indices: IntArray) { + this.disabledIndices = indices + notifyDataSetChanged() + } + + override fun checkItems(indices: IntArray) = Unit + + override fun uncheckItems(indices: IntArray) = Unit + + override fun toggleItems(indices: IntArray) = Unit + + override fun checkAllItems() = Unit + + override fun uncheckAllItems() = Unit + + override fun toggleAllChecked() = Unit + + override fun isItemChecked(index: Int) = false +} diff --git a/app/src/main/java/com/nextcloud/talk/call/CallParticipant.java b/app/src/main/java/com/nextcloud/talk/call/CallParticipant.java new file mode 100644 index 0000000..4aa207f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/CallParticipant.java @@ -0,0 +1,219 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.models.json.participants.Participant; +import com.nextcloud.talk.signaling.SignalingMessageReceiver; +import com.nextcloud.talk.webrtc.PeerConnectionWrapper; + +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; + +/** + * Model for (remote) call participants. + *

+ * This class keeps track of the state changes in a call participant and updates its data model as needed. View classes + * are expected to directly use the read-only data model. + */ +public class CallParticipant { + + private final SignalingMessageReceiver.CallParticipantMessageListener callParticipantMessageListener = + new SignalingMessageReceiver.CallParticipantMessageListener() { + @Override + public void onRaiseHand(boolean state, long timestamp) { + callParticipantModel.setRaisedHand(state, timestamp); + } + + @Override + public void onReaction(String reaction) { + callParticipantModel.emitReaction(reaction); + } + + @Override + public void onUnshareScreen() { + } + }; + + private final PeerConnectionWrapper.PeerConnectionObserver peerConnectionObserver = + new PeerConnectionWrapper.PeerConnectionObserver() { + @Override + public void onStreamAdded(MediaStream mediaStream) { + handleStreamChange(mediaStream); + } + + @Override + public void onStreamRemoved(MediaStream mediaStream) { + handleStreamChange(mediaStream); + } + + @Override + public void onIceConnectionStateChanged(PeerConnection.IceConnectionState iceConnectionState) { + handleIceConnectionStateChange(iceConnectionState); + } + }; + + private final PeerConnectionWrapper.PeerConnectionObserver screenPeerConnectionObserver = + new PeerConnectionWrapper.PeerConnectionObserver() { + @Override + public void onStreamAdded(MediaStream mediaStream) { + callParticipantModel.setScreenMediaStream(mediaStream); + } + + @Override + public void onStreamRemoved(MediaStream mediaStream) { + callParticipantModel.setScreenMediaStream(null); + } + + @Override + public void onIceConnectionStateChanged(PeerConnection.IceConnectionState iceConnectionState) { + callParticipantModel.setScreenIceConnectionState(iceConnectionState); + } + }; + + // DataChannel messages are sent only in video peers; (sender) screen peers do not even open them. + private final PeerConnectionWrapper.DataChannelMessageListener dataChannelMessageListener = + new PeerConnectionWrapper.DataChannelMessageListener() { + @Override + public void onAudioOn() { + callParticipantModel.setAudioAvailable(Boolean.TRUE); + } + + @Override + public void onAudioOff() { + callParticipantModel.setAudioAvailable(Boolean.FALSE); + } + + @Override + public void onVideoOn() { + callParticipantModel.setVideoAvailable(Boolean.TRUE); + } + + @Override + public void onVideoOff() { + callParticipantModel.setVideoAvailable(Boolean.FALSE); + } + + @Override + public void onNickChanged(String nick) { + callParticipantModel.setNick(nick); + } + }; + + private final MutableCallParticipantModel callParticipantModel; + + private final SignalingMessageReceiver signalingMessageReceiver; + + private PeerConnectionWrapper peerConnectionWrapper; + private PeerConnectionWrapper screenPeerConnectionWrapper; + + public CallParticipant(String sessionId, SignalingMessageReceiver signalingMessageReceiver) { + callParticipantModel = new MutableCallParticipantModel(sessionId); + + this.signalingMessageReceiver = signalingMessageReceiver; + signalingMessageReceiver.addListener(callParticipantMessageListener, sessionId); + } + + public void destroy() { + signalingMessageReceiver.removeListener(callParticipantMessageListener); + + if (peerConnectionWrapper != null) { + peerConnectionWrapper.removeObserver(peerConnectionObserver); + peerConnectionWrapper.removeListener(dataChannelMessageListener); + } + if (screenPeerConnectionWrapper != null) { + screenPeerConnectionWrapper.removeObserver(screenPeerConnectionObserver); + } + } + + public CallParticipantModel getCallParticipantModel() { + return callParticipantModel; + } + + public void setActor(Participant.ActorType actorType, String actorId) { + callParticipantModel.setActor(actorType, actorId); + } + + public void setUserId(String userId) { + callParticipantModel.setUserId(userId); + } + + public void setNick(String nick) { + callParticipantModel.setNick(nick); + } + + public void setInternal(Boolean internal) { + callParticipantModel.setInternal(internal); + } + + public void setPeerConnectionWrapper(PeerConnectionWrapper peerConnectionWrapper) { + if (this.peerConnectionWrapper != null) { + this.peerConnectionWrapper.removeObserver(peerConnectionObserver); + this.peerConnectionWrapper.removeListener(dataChannelMessageListener); + } + + this.peerConnectionWrapper = peerConnectionWrapper; + + if (this.peerConnectionWrapper == null) { + callParticipantModel.setIceConnectionState(null); + callParticipantModel.setMediaStream(null); + callParticipantModel.setAudioAvailable(null); + callParticipantModel.setVideoAvailable(null); + + return; + } + + handleIceConnectionStateChange(this.peerConnectionWrapper.getPeerConnection().iceConnectionState()); + handleStreamChange(this.peerConnectionWrapper.getStream()); + + this.peerConnectionWrapper.addObserver(peerConnectionObserver); + this.peerConnectionWrapper.addListener(dataChannelMessageListener); + } + + private void handleIceConnectionStateChange(PeerConnection.IceConnectionState iceConnectionState) { + callParticipantModel.setIceConnectionState(iceConnectionState); + + if (iceConnectionState == PeerConnection.IceConnectionState.NEW || + iceConnectionState == PeerConnection.IceConnectionState.CHECKING) { + callParticipantModel.setAudioAvailable(null); + callParticipantModel.setVideoAvailable(null); + } + } + + private void handleStreamChange(MediaStream mediaStream) { + if (mediaStream == null) { + callParticipantModel.setMediaStream(null); + callParticipantModel.setVideoAvailable(Boolean.FALSE); + + return; + } + + boolean hasAtLeastOneVideoStream = mediaStream.videoTracks != null && !mediaStream.videoTracks.isEmpty(); + + callParticipantModel.setMediaStream(mediaStream); + callParticipantModel.setVideoAvailable(hasAtLeastOneVideoStream); + } + + public void setScreenPeerConnectionWrapper(PeerConnectionWrapper screenPeerConnectionWrapper) { + if (this.screenPeerConnectionWrapper != null) { + this.screenPeerConnectionWrapper.removeObserver(screenPeerConnectionObserver); + } + + this.screenPeerConnectionWrapper = screenPeerConnectionWrapper; + + if (this.screenPeerConnectionWrapper == null) { + callParticipantModel.setScreenIceConnectionState(null); + callParticipantModel.setScreenMediaStream(null); + + return; + } + + callParticipantModel.setScreenIceConnectionState(this.screenPeerConnectionWrapper.getPeerConnection().iceConnectionState()); + callParticipantModel.setScreenMediaStream(this.screenPeerConnectionWrapper.getStream()); + + this.screenPeerConnectionWrapper.addObserver(screenPeerConnectionObserver); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/CallParticipantList.java b/app/src/main/java/com/nextcloud/talk/call/CallParticipantList.java new file mode 100644 index 0000000..4b3727a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/CallParticipantList.java @@ -0,0 +1,154 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.models.json.participants.Participant; +import com.nextcloud.talk.signaling.SignalingMessageReceiver; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Helper class to keep track of the participants in a call based on the signaling messages. + *

+ * The CallParticipantList adds a listener for participant list messages as soon as it is created and starts tracking + * the call participants until destroyed. Notifications about the changes can be received by adding an observer to the + * CallParticipantList; note that no sorting is guaranteed on the participants. + */ +public class CallParticipantList { + + private final CallParticipantListNotifier callParticipantListNotifier = new CallParticipantListNotifier(); + + private final SignalingMessageReceiver signalingMessageReceiver; + + public interface Observer { + void onCallParticipantsChanged(Collection joined, Collection updated, + Collection left, Collection unchanged); + void onCallEndedForAll(); + } + + private final SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener = + new SignalingMessageReceiver.ParticipantListMessageListener() { + + private final Map callParticipants = new HashMap<>(); + + @Override + public void onUsersInRoom(List participants) { + processParticipantList(participants); + } + + @Override + public void onParticipantsUpdate(List participants) { + processParticipantList(participants); + } + + private void processParticipantList(List participants) { + Collection joined = new ArrayList<>(); + Collection updated = new ArrayList<>(); + Collection left = new ArrayList<>(); + Collection unchanged = new ArrayList<>(); + + Collection knownCallParticipantsNotFound = new ArrayList<>(callParticipants.values()); + + for (Participant participant : participants) { + String sessionId = participant.getSessionId(); + Participant callParticipant = callParticipants.get(sessionId); + + boolean knownCallParticipant = callParticipant != null; + if (!knownCallParticipant && participant.getInCall() != Participant.InCallFlags.DISCONNECTED) { + callParticipants.put(sessionId, copyParticipant(participant)); + joined.add(copyParticipant(participant)); + } else if (knownCallParticipant && participant.getInCall() == Participant.InCallFlags.DISCONNECTED) { + callParticipants.remove(sessionId); + // No need to copy it, as it will be no longer used. + callParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + left.add(callParticipant); + } else if (knownCallParticipant && callParticipant.getInCall() != participant.getInCall()) { + callParticipant.setInCall(participant.getInCall()); + updated.add(copyParticipant(participant)); + } else if (knownCallParticipant) { + unchanged.add(copyParticipant(participant)); + } + + if (knownCallParticipant) { + knownCallParticipantsNotFound.remove(callParticipant); + } + } + + for (Participant callParticipant : knownCallParticipantsNotFound) { + callParticipants.remove(callParticipant.getSessionId()); + // No need to copy it, as it will be no longer used. + callParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + } + left.addAll(knownCallParticipantsNotFound); + + if (!joined.isEmpty() || !updated.isEmpty() || !left.isEmpty()) { + callParticipantListNotifier.notifyChanged(joined, updated, left, unchanged); + } + } + + @Override + public void onAllParticipantsUpdate(long inCall) { + if (inCall != Participant.InCallFlags.DISCONNECTED) { + // Updating all participants is expected to happen only to disconnect them. + return; + } + + callParticipantListNotifier.notifyCallEndedForAll(); + + Collection joined = new ArrayList<>(); + Collection updated = new ArrayList<>(); + Collection left = new ArrayList<>(callParticipants.size()); + Collection unchanged = new ArrayList<>(); + + for (Participant callParticipant : callParticipants.values()) { + // No need to copy it, as it will be no longer used. + callParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + left.add(callParticipant); + } + callParticipants.clear(); + + if (!left.isEmpty()) { + callParticipantListNotifier.notifyChanged(joined, updated, left, unchanged); + } + } + + private Participant copyParticipant(Participant participant) { + Participant copiedParticipant = new Participant(); + copiedParticipant.setActorId(participant.getActorId()); + copiedParticipant.setActorType(participant.getActorType()); + copiedParticipant.setInCall(participant.getInCall()); + copiedParticipant.setInternal(participant.getInternal()); + copiedParticipant.setLastPing(participant.getLastPing()); + copiedParticipant.setSessionId(participant.getSessionId()); + copiedParticipant.setType(participant.getType()); + copiedParticipant.setUserId(participant.getUserId()); + + return copiedParticipant; + } + }; + + public CallParticipantList(SignalingMessageReceiver signalingMessageReceiver) { + this.signalingMessageReceiver = signalingMessageReceiver; + this.signalingMessageReceiver.addListener(participantListMessageListener); + } + + public void destroy() { + signalingMessageReceiver.removeListener(participantListMessageListener); + } + + public void addObserver(Observer observer) { + callParticipantListNotifier.addObserver(observer); + } + + public void removeObserver(Observer observer) { + callParticipantListNotifier.removeObserver(observer); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/CallParticipantListNotifier.java b/app/src/main/java/com/nextcloud/talk/call/CallParticipantListNotifier.java new file mode 100644 index 0000000..f02b300 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/CallParticipantListNotifier.java @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.models.json.participants.Participant; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Helper class to register and notify CallParticipantList.Observers. + *

+ * This class is only meant for internal use by CallParticipantList; listeners must register themselves against + * a CallParticipantList rather than against a CallParticipantListNotifier. + */ +class CallParticipantListNotifier { + + private final Set callParticipantListObservers = new LinkedHashSet<>(); + + public synchronized void addObserver(CallParticipantList.Observer observer) { + if (observer == null) { + throw new IllegalArgumentException("CallParticipantList.Observer can not be null"); + } + + callParticipantListObservers.add(observer); + } + + public synchronized void removeObserver(CallParticipantList.Observer observer) { + callParticipantListObservers.remove(observer); + } + + public synchronized void notifyChanged(Collection joined, Collection updated, + Collection left, Collection unchanged) { + for (CallParticipantList.Observer observer : new ArrayList<>(callParticipantListObservers)) { + observer.onCallParticipantsChanged(joined, updated, left, unchanged); + } + } + + public synchronized void notifyCallEndedForAll() { + for (CallParticipantList.Observer observer : new ArrayList<>(callParticipantListObservers)) { + observer.onCallEndedForAll(); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/CallParticipantModel.java b/app/src/main/java/com/nextcloud/talk/call/CallParticipantModel.java new file mode 100644 index 0000000..f5409ad --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/CallParticipantModel.java @@ -0,0 +1,190 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import android.os.Handler; + +import com.nextcloud.talk.models.json.participants.Participant; + +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; + +import java.util.Objects; + +/** + * Read-only data model for (remote) call participants. + *

+ * If the hand was never raised null is returned by "getRaisedHand()". Otherwise a RaisedHand object is returned with + * the current state (raised or not) and the timestamp when the raised hand state last changed. + *

+ * The received audio and video are available only if the participant is sending them and also has them enabled. + * Before a connection is established it is not known whether audio and video are available or not, so null is returned + * in that case (therefore it should not be autoboxed to a plain boolean without checking that). + *

+ * Audio and video in screen shares, on the other hand, are always seen as available. + *

+ * Actor type and actor id will be set only in Talk >= 20. + *

+ * Clients of the model can observe it with CallParticipantModel.Observer to be notified when any value changes. + * Getters called after receiving a notification are guaranteed to provide at least the value that triggered the + * notification, but it may return even a more up to date one (so getting the value again on the following + * notification may return the same value as before). + *

+ * Besides onChange(), which notifies about changes in the model values, CallParticipantModel.Observer provides + * additional methods to be notified about one-time events that are not reflected in the model values, like reactions. + */ +public class CallParticipantModel { + + protected final CallParticipantModelNotifier callParticipantModelNotifier = new CallParticipantModelNotifier(); + + protected final String sessionId; + + protected Data actorType; + protected Data actorId; + protected Data userId; + protected Data nick; + + protected Data internal; + + protected Data raisedHand; + + protected Data iceConnectionState; + protected Data mediaStream; + protected Data audioAvailable; + protected Data videoAvailable; + + protected Data screenIceConnectionState; + protected Data screenMediaStream; + + public interface Observer { + void onChange(); + void onReaction(String reaction); + } + + protected class Data { + + private T value; + + public T getValue() { + return value; + } + + public void setValue(T value) { + if (Objects.equals(this.value, value)) { + return; + } + + this.value = value; + + callParticipantModelNotifier.notifyChange(); + } + } + + public CallParticipantModel(String sessionId) { + this.sessionId = sessionId; + + this.actorType = new Data<>(); + this.actorId = new Data<>(); + this.userId = new Data<>(); + this.nick = new Data<>(); + + this.internal = new Data<>(); + + this.raisedHand = new Data<>(); + + this.iceConnectionState = new Data<>(); + this.mediaStream = new Data<>(); + this.audioAvailable = new Data<>(); + this.videoAvailable = new Data<>(); + + this.screenIceConnectionState = new Data<>(); + this.screenMediaStream = new Data<>(); + } + + public String getSessionId() { + return sessionId; + } + + public Participant.ActorType getActorType() { + return actorType.getValue(); + } + + public String getActorId() { + return actorId.getValue(); + } + + public String getUserId() { + return userId.getValue(); + } + + public String getNick() { + return nick.getValue(); + } + + public Boolean isInternal() { + return internal.getValue(); + } + + public RaisedHand getRaisedHand() { + return raisedHand.getValue(); + } + + public PeerConnection.IceConnectionState getIceConnectionState() { + return iceConnectionState.getValue(); + } + + public MediaStream getMediaStream() { + return mediaStream.getValue(); + } + + public Boolean isAudioAvailable() { + return audioAvailable.getValue(); + } + + public Boolean isVideoAvailable() { + return videoAvailable.getValue(); + } + + public PeerConnection.IceConnectionState getScreenIceConnectionState() { + return screenIceConnectionState.getValue(); + } + + public MediaStream getScreenMediaStream() { + return screenMediaStream.getValue(); + } + + /** + * Adds an Observer to be notified when any value changes. + * + * @param observer the Observer + * @see CallParticipantModel#addObserver(Observer, Handler) + */ + public void addObserver(Observer observer) { + addObserver(observer, null); + } + + /** + * Adds an observer to be notified when any value changes. + *

+ * The observer will be notified on the thread associated to the given handler. If no handler is given the + * observer will be immediately notified on the same thread that changed the value; the observer will be + * immediately notified too if the thread of the handler is the same thread that changed the value. + *

+ * An observer is expected to be added only once. If the same observer is added again it will be notified just + * once on the thread of the last handler. + * + * @param observer the Observer + * @param handler a Handler for the thread to be notified on + */ + public void addObserver(Observer observer, Handler handler) { + callParticipantModelNotifier.addObserver(observer, handler); + } + + public void removeObserver(Observer observer) { + callParticipantModelNotifier.removeObserver(observer); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/CallParticipantModelNotifier.java b/app/src/main/java/com/nextcloud/talk/call/CallParticipantModelNotifier.java new file mode 100644 index 0000000..05180c1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/CallParticipantModelNotifier.java @@ -0,0 +1,85 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import android.os.Handler; +import android.os.Looper; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Helper class to register and notify CallParticipantModel.Observers. + *

+ * This class is only meant for internal use by CallParticipantModel; observers must register themselves against a + * CallParticipantModel rather than against a CallParticipantModelNotifier. + */ +class CallParticipantModelNotifier { + + private final List callParticipantModelObserversOn = new ArrayList<>(); + + /** + * Helper class to associate a CallParticipantModel.Observer with a Handler. + */ + private static class CallParticipantModelObserverOn { + public final CallParticipantModel.Observer observer; + public final Handler handler; + + private CallParticipantModelObserverOn(CallParticipantModel.Observer observer, Handler handler) { + this.observer = observer; + this.handler = handler; + } + } + + public synchronized void addObserver(CallParticipantModel.Observer observer, Handler handler) { + if (observer == null) { + throw new IllegalArgumentException("CallParticipantModel.Observer can not be null"); + } + + removeObserver(observer); + + callParticipantModelObserversOn.add(new CallParticipantModelObserverOn(observer, handler)); + } + + public synchronized void removeObserver(CallParticipantModel.Observer observer) { + Iterator it = callParticipantModelObserversOn.iterator(); + while (it.hasNext()) { + CallParticipantModelObserverOn observerOn = it.next(); + + if (observerOn.observer == observer) { + it.remove(); + + return; + } + } + } + + public synchronized void notifyChange() { + for (CallParticipantModelObserverOn observerOn : new ArrayList<>(callParticipantModelObserversOn)) { + if (observerOn.handler == null || observerOn.handler.getLooper() == Looper.myLooper()) { + observerOn.observer.onChange(); + } else { + observerOn.handler.post(() -> { + observerOn.observer.onChange(); + }); + } + } + } + + public synchronized void notifyReaction(String reaction) { + for (CallParticipantModelObserverOn observerOn : new ArrayList<>(callParticipantModelObserversOn)) { + if (observerOn.handler == null || observerOn.handler.getLooper() == Looper.myLooper()) { + observerOn.observer.onReaction(reaction); + } else { + observerOn.handler.post(() -> { + observerOn.observer.onReaction(reaction); + }); + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/LocalCallParticipantModel.java b/app/src/main/java/com/nextcloud/talk/call/LocalCallParticipantModel.java new file mode 100644 index 0000000..b1dcece --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/LocalCallParticipantModel.java @@ -0,0 +1,114 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import android.os.Handler; + +import java.util.Objects; + +/** + * Read-only data model for local call participants. + *

+ * Clients of the model can observe it with LocalCallParticipantModel.Observer to be notified when any value changes. + * Getters called after receiving a notification are guaranteed to provide at least the value that triggered the + * notification, but it may return even a more up to date one (so getting the value again on the following notification + * may return the same value as before). + */ +public class LocalCallParticipantModel { + + protected final LocalCallParticipantModelNotifier localCallParticipantModelNotifier = + new LocalCallParticipantModelNotifier(); + + protected Data audioEnabled; + protected Data speaking; + protected Data speakingWhileMuted; + protected Data videoEnabled; + + public interface Observer { + void onChange(); + } + + protected class Data { + + private T value; + + public Data() { + } + + public Data(T value) { + this.value = value; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + if (Objects.equals(this.value, value)) { + return; + } + + this.value = value; + + localCallParticipantModelNotifier.notifyChange(); + } + } + + public LocalCallParticipantModel() { + this.audioEnabled = new Data<>(Boolean.FALSE); + this.speaking = new Data<>(Boolean.FALSE); + this.speakingWhileMuted = new Data<>(Boolean.FALSE); + this.videoEnabled = new Data<>(Boolean.FALSE); + } + + public Boolean isAudioEnabled() { + return audioEnabled.getValue(); + } + + public Boolean isSpeaking() { + return speaking.getValue(); + } + + public Boolean isSpeakingWhileMuted() { + return speakingWhileMuted.getValue(); + } + + public Boolean isVideoEnabled() { + return videoEnabled.getValue(); + } + + /** + * Adds an Observer to be notified when any value changes. + * + * @param observer the Observer + * @see LocalCallParticipantModel#addObserver(Observer, Handler) + */ + public void addObserver(Observer observer) { + addObserver(observer, null); + } + + /** + * Adds an observer to be notified when any value changes. + *

+ * The observer will be notified on the thread associated to the given handler. If no handler is given the + * observer will be immediately notified on the same thread that changed the value; the observer will be + * immediately notified too if the thread of the handler is the same thread that changed the value. + *

+ * An observer is expected to be added only once. If the same observer is added again it will be notified just + * once on the thread of the last handler. + * + * @param observer the Observer + * @param handler a Handler for the thread to be notified on + */ + public void addObserver(Observer observer, Handler handler) { + localCallParticipantModelNotifier.addObserver(observer, handler); + } + + public void removeObserver(Observer observer) { + localCallParticipantModelNotifier.removeObserver(observer); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/LocalCallParticipantModelNotifier.java b/app/src/main/java/com/nextcloud/talk/call/LocalCallParticipantModelNotifier.java new file mode 100644 index 0000000..b46f1f0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/LocalCallParticipantModelNotifier.java @@ -0,0 +1,73 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import android.os.Handler; +import android.os.Looper; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Helper class to register and notify LocalCallParticipantModel.Observers. + *

+ * This class is only meant for internal use by LocalCallParticipantModel; observers must register themselves against a + * LocalCallParticipantModel rather than against a LocalCallParticipantModelNotifier. + */ +class LocalCallParticipantModelNotifier { + + private final List localCallParticipantModelObserversOn = new ArrayList<>(); + + /** + * Helper class to associate a LocalCallParticipantModel.Observer with a Handler. + */ + private static class LocalCallParticipantModelObserverOn { + public final LocalCallParticipantModel.Observer observer; + public final Handler handler; + + private LocalCallParticipantModelObserverOn(LocalCallParticipantModel.Observer observer, Handler handler) { + this.observer = observer; + this.handler = handler; + } + } + + public synchronized void addObserver(LocalCallParticipantModel.Observer observer, Handler handler) { + if (observer == null) { + throw new IllegalArgumentException("LocalCallParticipantModel.Observer can not be null"); + } + + removeObserver(observer); + + localCallParticipantModelObserversOn.add(new LocalCallParticipantModelObserverOn(observer, handler)); + } + + public synchronized void removeObserver(LocalCallParticipantModel.Observer observer) { + Iterator it = localCallParticipantModelObserversOn.iterator(); + while (it.hasNext()) { + LocalCallParticipantModelObserverOn observerOn = it.next(); + + if (observerOn.observer == observer) { + it.remove(); + + return; + } + } + } + + public synchronized void notifyChange() { + for (LocalCallParticipantModelObserverOn observerOn : new ArrayList<>(localCallParticipantModelObserversOn)) { + if (observerOn.handler == null || observerOn.handler.getLooper() == Looper.myLooper()) { + observerOn.observer.onChange(); + } else { + observerOn.handler.post(() -> { + observerOn.observer.onChange(); + }); + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcaster.java b/app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcaster.java new file mode 100644 index 0000000..1022d39 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcaster.java @@ -0,0 +1,170 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage; +import com.nextcloud.talk.models.json.signaling.NCMessagePayload; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; + +import java.util.Objects; + +/** + * Helper class to send the local participant state to the other participants in the call. + *

+ * Once created, and until destroyed, the LocalStateBroadcaster will send the changes in the local participant state to + * all the participants in the call. Note that the LocalStateBroadcaster does not check whether the local participant + * is actually in the call or not; it is expected that the LocalStateBroadcaster will be created and destroyed when the + * local participant joins and leaves the call. + *

+ * The LocalStateBroadcaster also sends the current state to remote participants when they join (which implicitly + * sends it to all remote participants when the local participant joins the call) so they can set an initial state + * for the local participant. + */ +public abstract class LocalStateBroadcaster { + + private final LocalCallParticipantModel localCallParticipantModel; + + private final LocalCallParticipantModelObserver localCallParticipantModelObserver; + + private final MessageSender messageSender; + + private class LocalCallParticipantModelObserver implements LocalCallParticipantModel.Observer { + + private Boolean audioEnabled; + private Boolean speaking; + private Boolean videoEnabled; + + public LocalCallParticipantModelObserver(LocalCallParticipantModel localCallParticipantModel) { + audioEnabled = localCallParticipantModel.isAudioEnabled(); + speaking = localCallParticipantModel.isSpeaking(); + videoEnabled = localCallParticipantModel.isVideoEnabled(); + } + + @Override + public void onChange() { + if (!Objects.equals(audioEnabled, localCallParticipantModel.isAudioEnabled())) { + audioEnabled = localCallParticipantModel.isAudioEnabled(); + + messageSender.sendToAll(getDataChannelMessageForAudioState()); + messageSender.sendToAll(getSignalingMessageForAudioState()); + } + + if (!Objects.equals(speaking, localCallParticipantModel.isSpeaking())) { + speaking = localCallParticipantModel.isSpeaking(); + + messageSender.sendToAll(getDataChannelMessageForSpeakingState()); + } + + if (!Objects.equals(videoEnabled, localCallParticipantModel.isVideoEnabled())) { + videoEnabled = localCallParticipantModel.isVideoEnabled(); + + messageSender.sendToAll(getDataChannelMessageForVideoState()); + messageSender.sendToAll(getSignalingMessageForVideoState()); + } + } + } + + public LocalStateBroadcaster(LocalCallParticipantModel localCallParticipantModel, + MessageSender messageSender) { + this.localCallParticipantModel = localCallParticipantModel; + this.localCallParticipantModelObserver = new LocalCallParticipantModelObserver(localCallParticipantModel); + this.messageSender = messageSender; + + this.localCallParticipantModel.addObserver(localCallParticipantModelObserver); + } + + public void destroy() { + this.localCallParticipantModel.removeObserver(localCallParticipantModelObserver); + } + + public abstract void handleCallParticipantAdded(CallParticipantModel callParticipantModel); + public abstract void handleCallParticipantRemoved(CallParticipantModel callParticipantModel); + + protected DataChannelMessage getDataChannelMessageForAudioState() { + String type = "audioOff"; + if (localCallParticipantModel.isAudioEnabled() != null && localCallParticipantModel.isAudioEnabled()) { + type = "audioOn"; + } + + return new DataChannelMessage(type); + } + + protected DataChannelMessage getDataChannelMessageForSpeakingState() { + String type = "stoppedSpeaking"; + if (localCallParticipantModel.isSpeaking() != null && localCallParticipantModel.isSpeaking()) { + type = "speaking"; + } + + return new DataChannelMessage(type); + } + + protected DataChannelMessage getDataChannelMessageForVideoState() { + String type = "videoOff"; + if (localCallParticipantModel.isVideoEnabled() != null && localCallParticipantModel.isVideoEnabled()) { + type = "videoOn"; + } + + return new DataChannelMessage(type); + } + + /** + * Returns a signaling message with the common fields set (type and room type). + * + * @param type the type of the signaling message + * @return the signaling message + */ + private NCSignalingMessage createBaseSignalingMessage(String type) { + NCSignalingMessage ncSignalingMessage = new NCSignalingMessage(); + // "roomType" is not really relevant without a peer or when referring to the whole participant, but it is + // nevertheless expected in the message. As most of the signaling messages currently sent to all participants + // are related to audio/video state "video" is used as the room type. + ncSignalingMessage.setRoomType("video"); + ncSignalingMessage.setType(type); + + return ncSignalingMessage; + } + + /** + * Returns a signaling message to notify current audio state. + * + * @return the signaling message + */ + protected NCSignalingMessage getSignalingMessageForAudioState() { + String type = "mute"; + if (localCallParticipantModel.isAudioEnabled() != null && localCallParticipantModel.isAudioEnabled()) { + type = "unmute"; + } + + NCSignalingMessage ncSignalingMessage = createBaseSignalingMessage(type); + + NCMessagePayload ncMessagePayload = new NCMessagePayload(); + ncMessagePayload.setName("audio"); + ncSignalingMessage.setPayload(ncMessagePayload); + + return ncSignalingMessage; + } + + /** + * Returns a signaling message to notify current video state. + * + * @return the signaling message + */ + protected NCSignalingMessage getSignalingMessageForVideoState() { + String type = "mute"; + if (localCallParticipantModel.isVideoEnabled() != null && localCallParticipantModel.isVideoEnabled()) { + type = "unmute"; + } + + NCSignalingMessage ncSignalingMessage = createBaseSignalingMessage(type); + + NCMessagePayload ncMessagePayload = new NCMessagePayload(); + ncMessagePayload.setName("video"); + ncSignalingMessage.setPayload(ncMessagePayload); + + return ncSignalingMessage; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcasterMcu.java b/app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcasterMcu.java new file mode 100644 index 0000000..911bf1b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcasterMcu.java @@ -0,0 +1,118 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +/** + * Helper class to send the local participant state to the other participants in the call when an MCU is used. + *

+ * Sending the state when it changes is handled by the base class; this subclass only handles sending the initial + * state when a remote participant is added. + *

+ * When Janus is used data channel messages are sent to all remote participants (with a peer connection to receive from + * the local participant). Moreover, it is not possible to know when the remote participants open the data channel to + * receive the messages, or even when they establish the receiver connection; it is only possible to know when the + * data channel is open for the publisher connection of the local participant. Due to all that the state is sent + * several times with an increasing delay whenever a participant joins the call (which implicitly broadcasts the + * initial state when the local participant joins the call, as all the remote participants joined from the point of + * view of the local participant). If the state was already being sent the sending is restarted with each new + * participant that joins. + *

+ * Similarly, in the case of signaling messages it is not possible either to know when the remote participants have + * "seen" the local participant and thus are ready to handle signaling messages about the state. However, in the case + * of signaling messages it is possible to send them to a specific participant, so the initial state is sent several + * times with an increasing delay directly to the participant that was added. Moreover, if the participant is removed + * the state is no longer directly sent. + *

+ * In any case, note that the state is sent only when the remote participant joins the call. Even in case of + * temporary disconnections the normal state updates sent when the state changes are expected to be received by the + * other participant, as signaling messages are sent through a WebSocket and are therefore reliable. Moreover, even + * if the WebSocket is restarted and the connection resumed (rather than joining with a new session ID) the messages + * would be also received, as in that case they would be queued until the WebSocket is connected again. + *

+ * Data channel messages, on the other hand, could be lost if the remote participant restarts the peer receiver + * connection (although they would be received in case of temporary disconnections, as data channels use a reliable + * transport by default). Therefore, as the speaking state is sent only through data channels, updates of the speaking + * state could be not received by remote participants. + */ +public class LocalStateBroadcasterMcu extends LocalStateBroadcaster { + + private final MessageSender messageSender; + + private final Map sendStateWithRepetitionByParticipant = new HashMap<>(); + + private Disposable sendStateWithRepetition; + + public LocalStateBroadcasterMcu(LocalCallParticipantModel localCallParticipantModel, + MessageSender messageSender) { + super(localCallParticipantModel, messageSender); + + this.messageSender = messageSender; + } + + public void destroy() { + super.destroy(); + + if (sendStateWithRepetition != null) { + sendStateWithRepetition.dispose(); + } + + for (Disposable sendStateWithRepetitionForParticipant: sendStateWithRepetitionByParticipant.values()) { + sendStateWithRepetitionForParticipant.dispose(); + } + } + + @Override + public void handleCallParticipantAdded(CallParticipantModel callParticipantModel) { + if (sendStateWithRepetition != null) { + sendStateWithRepetition.dispose(); + } + + sendStateWithRepetition = Observable + .fromArray(new Integer[]{0, 1, 2, 4, 8, 16}) + .concatMap(i -> Observable.just(i).delay(i, TimeUnit.SECONDS, Schedulers.io())) + .subscribe(value -> sendState()); + + String sessionId = callParticipantModel.getSessionId(); + Disposable sendStateWithRepetitionForParticipant = sendStateWithRepetitionByParticipant.get(sessionId); + if (sendStateWithRepetitionForParticipant != null) { + sendStateWithRepetitionForParticipant.dispose(); + } + + sendStateWithRepetitionByParticipant.put(sessionId, Observable + .fromArray(new Integer[]{0, 1, 2, 4, 8, 16}) + .concatMap(i -> Observable.just(i).delay(i, TimeUnit.SECONDS, Schedulers.io())) + .subscribe(value -> sendState(sessionId))); + } + + @Override + public void handleCallParticipantRemoved(CallParticipantModel callParticipantModel) { + String sessionId = callParticipantModel.getSessionId(); + Disposable sendStateWithRepetitionForParticipant = sendStateWithRepetitionByParticipant.get(sessionId); + if (sendStateWithRepetitionForParticipant != null) { + sendStateWithRepetitionForParticipant.dispose(); + } + } + + private void sendState() { + messageSender.sendToAll(getDataChannelMessageForAudioState()); + messageSender.sendToAll(getDataChannelMessageForSpeakingState()); + messageSender.sendToAll(getDataChannelMessageForVideoState()); + } + + private void sendState(String sessionId) { + messageSender.send(getSignalingMessageForAudioState(), sessionId); + messageSender.send(getSignalingMessageForVideoState(), sessionId); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcasterNoMcu.java b/app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcasterNoMcu.java new file mode 100644 index 0000000..1377e62 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/LocalStateBroadcasterNoMcu.java @@ -0,0 +1,128 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import org.webrtc.PeerConnection; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Helper class to send the local participant state to the other participants in the call when an MCU is not used. + *

+ * Sending the state when it changes is handled by the base class; this subclass only handles sending the initial + * state when a remote participant is added. + *

+ * The state is sent when a connection with another participant is first established (which implicitly broadcasts the + * initial state when the local participant joins the call, as a connection is established with all the remote + * participants). Note that, as long as that participant stays in the call, the initial state is not sent again, even + * after a temporary disconnection; data channels use a reliable transport by default, so even if the state changes + * while the connection is temporarily interrupted the normal state update messages should be received by the other + * participant once the connection is restored. + *

+ * Nevertheless, in case of a failed connection and an ICE restart it is unclear whether the data channel messages + * would be received or not (as the data channel transport may be the one that failed and needs to be restarted). + * However, the state (except the speaking state) is also sent through signaling messages, which need to be + * explicitly fetched from the internal signaling server, so even in case of a failed connection they will be + * eventually received once the remote participant connects again. + */ +public class LocalStateBroadcasterNoMcu extends LocalStateBroadcaster { + + private final MessageSenderNoMcu messageSender; + + private final Map iceConnectionStateObservers = new HashMap<>(); + + private class IceConnectionStateObserver implements CallParticipantModel.Observer { + + private final CallParticipantModel callParticipantModel; + + private PeerConnection.IceConnectionState iceConnectionState; + + public IceConnectionStateObserver(CallParticipantModel callParticipantModel) { + this.callParticipantModel = callParticipantModel; + + callParticipantModel.addObserver(this); + iceConnectionStateObservers.put(callParticipantModel.getSessionId(), this); + } + + @Override + public void onChange() { + if (Objects.equals(iceConnectionState, callParticipantModel.getIceConnectionState())) { + return; + } + + iceConnectionState = callParticipantModel.getIceConnectionState(); + + if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED || + iceConnectionState == PeerConnection.IceConnectionState.COMPLETED) { + remove(); + + sendState(callParticipantModel.getSessionId()); + } + } + + @Override + public void onReaction(String reaction) { + } + + public void remove() { + callParticipantModel.removeObserver(this); + iceConnectionStateObservers.remove(callParticipantModel.getSessionId()); + } + } + + public LocalStateBroadcasterNoMcu(LocalCallParticipantModel localCallParticipantModel, + MessageSenderNoMcu messageSender) { + super(localCallParticipantModel, messageSender); + + this.messageSender = messageSender; + } + + public void destroy() { + super.destroy(); + + // The observers remove themselves from the map, so a copy is needed to remove them while iterating. + List iceConnectionStateObserversCopy = + new ArrayList<>(iceConnectionStateObservers.values()); + for (IceConnectionStateObserver iceConnectionStateObserver : iceConnectionStateObserversCopy) { + iceConnectionStateObserver.remove(); + } + } + + @Override + public void handleCallParticipantAdded(CallParticipantModel callParticipantModel) { + IceConnectionStateObserver iceConnectionStateObserver = + iceConnectionStateObservers.get(callParticipantModel.getSessionId()); + if (iceConnectionStateObserver != null) { + iceConnectionStateObserver.remove(); + } + + iceConnectionStateObserver = new IceConnectionStateObserver(callParticipantModel); + iceConnectionStateObservers.put(callParticipantModel.getSessionId(), iceConnectionStateObserver); + } + + @Override + public void handleCallParticipantRemoved(CallParticipantModel callParticipantModel) { + IceConnectionStateObserver iceConnectionStateObserver = + iceConnectionStateObservers.get(callParticipantModel.getSessionId()); + if (iceConnectionStateObserver != null) { + iceConnectionStateObserver.remove(); + } + } + + private void sendState(String sessionId) { + messageSender.send(getDataChannelMessageForAudioState(), sessionId); + messageSender.send(getDataChannelMessageForSpeakingState(), sessionId); + messageSender.send(getDataChannelMessageForVideoState(), sessionId); + + messageSender.send(getSignalingMessageForAudioState(), sessionId); + messageSender.send(getSignalingMessageForVideoState(), sessionId); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/MessageSender.java b/app/src/main/java/com/nextcloud/talk/call/MessageSender.java new file mode 100644 index 0000000..dd3eb14 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/MessageSender.java @@ -0,0 +1,93 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; +import com.nextcloud.talk.signaling.SignalingMessageSender; +import com.nextcloud.talk.webrtc.PeerConnectionWrapper; + +import java.util.List; +import java.util.Set; + +/** + * Helper class to send messages to participants in a call. + *

+ * A specific subclass has to be created depending on whether an MCU is being used or not. + *

+ * Note that recipients of signaling messages are not validated, so no error will be triggered if trying to send a + * message to a participant with a session ID that does not exist or is not in the call. + *

+ * Also note that, unlike signaling messages, data channel messages require a peer connection. Therefore data channel + * messages may not be received by a participant if there is no peer connection with that participant (for example, if + * neither the local and remote participants have publishing rights). Moreover, data channel messages are expected to + * be received only on peer connections with type "video", so data channel messages will not be sent on other peer + * connections. + */ +public abstract class MessageSender { + + private final SignalingMessageSender signalingMessageSender; + + private final Set callParticipantSessionIds; + + protected final List peerConnectionWrappers; + + public MessageSender(SignalingMessageSender signalingMessageSender, + Set callParticipantSessionIds, + List peerConnectionWrappers) { + this.signalingMessageSender = signalingMessageSender; + this.callParticipantSessionIds = callParticipantSessionIds; + this.peerConnectionWrappers = peerConnectionWrappers; + } + + /** + * Sends the given data channel message to all the participants in the call. + * + * @param dataChannelMessage the message to send + */ + public abstract void sendToAll(DataChannelMessage dataChannelMessage); + + /** + * Sends the given signaling message to the given session ID. + *

+ * Note that the signaling message will be modified to set the recipient in the "to" field. + * + * @param ncSignalingMessage the message to send + * @param sessionId the signaling session ID of the participant to send the message to + */ + public void send(NCSignalingMessage ncSignalingMessage, String sessionId) { + ncSignalingMessage.setTo(sessionId); + + signalingMessageSender.send(ncSignalingMessage); + } + + /** + * Sends the given signaling message to all the participants in the call. + *

+ * Note that the signaling message will be modified to set each of the recipients in the "to" field. + * + * @param ncSignalingMessage the message to send + */ + public void sendToAll(NCSignalingMessage ncSignalingMessage) { + for (String sessionId: callParticipantSessionIds) { + ncSignalingMessage.setTo(sessionId); + + signalingMessageSender.send(ncSignalingMessage); + } + } + + protected PeerConnectionWrapper getPeerConnectionWrapper(String sessionId) { + for (PeerConnectionWrapper peerConnectionWrapper: peerConnectionWrappers) { + if (peerConnectionWrapper.getSessionId().equals(sessionId) + && "video".equals(peerConnectionWrapper.getVideoStreamType())) { + return peerConnectionWrapper; + } + } + + return null; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java b/app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java new file mode 100644 index 0000000..0b7d3ea --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java @@ -0,0 +1,41 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage; +import com.nextcloud.talk.signaling.SignalingMessageSender; +import com.nextcloud.talk.webrtc.PeerConnectionWrapper; + +import java.util.List; +import java.util.Set; + +/** + * Helper class to send messages to participants in a call when an MCU is used. + *

+ * Note that when Janus is used it is not possible to send a data channel message to a specific participant. Any data + * channel message will be broadcast to all the subscribers of the publisher peer connection (the own peer connection). + */ +public class MessageSenderMcu extends MessageSender { + + private final String ownSessionId; + + public MessageSenderMcu(SignalingMessageSender signalingMessageSender, + Set callParticipantSessionIds, + List peerConnectionWrappers, + String ownSessionId) { + super(signalingMessageSender, callParticipantSessionIds, peerConnectionWrappers); + + this.ownSessionId = ownSessionId; + } + + public void sendToAll(DataChannelMessage dataChannelMessage) { + PeerConnectionWrapper ownPeerConnectionWrapper = getPeerConnectionWrapper(ownSessionId); + if (ownPeerConnectionWrapper != null) { + ownPeerConnectionWrapper.send(dataChannelMessage); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java b/app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java new file mode 100644 index 0000000..d6c837b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java @@ -0,0 +1,47 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage; +import com.nextcloud.talk.signaling.SignalingMessageSender; +import com.nextcloud.talk.webrtc.PeerConnectionWrapper; + +import java.util.List; +import java.util.Set; + +/** + * Helper class to send messages to participants in a call when an MCU is not used. + */ +public class MessageSenderNoMcu extends MessageSender { + + public MessageSenderNoMcu(SignalingMessageSender signalingMessageSender, + Set callParticipantSessionIds, + List peerConnectionWrappers) { + super(signalingMessageSender, callParticipantSessionIds, peerConnectionWrappers); + } + + /** + * Sends the given data channel message to the given signaling session ID. + * + * @param dataChannelMessage the message to send + * @param sessionId the signaling session ID of the participant to send the message to + */ + public void send(DataChannelMessage dataChannelMessage, String sessionId) { + PeerConnectionWrapper peerConnectionWrapper = getPeerConnectionWrapper(sessionId); + if (peerConnectionWrapper != null) { + peerConnectionWrapper.send(dataChannelMessage); + } + } + + public void sendToAll(DataChannelMessage dataChannelMessage) { + for (PeerConnectionWrapper peerConnectionWrapper: peerConnectionWrappers) { + if ("video".equals(peerConnectionWrapper.getVideoStreamType())){ + peerConnectionWrapper.send(dataChannelMessage); + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/MutableCallParticipantModel.java b/app/src/main/java/com/nextcloud/talk/call/MutableCallParticipantModel.java new file mode 100644 index 0000000..c8bbded --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/MutableCallParticipantModel.java @@ -0,0 +1,73 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.models.json.participants.Participant; + +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; + +/** + * Mutable data model for (remote) call participants. + *

+ * There is no synchronization when setting the values; if needed, it should be handled by the clients of the model. + */ +public class MutableCallParticipantModel extends CallParticipantModel { + + public MutableCallParticipantModel(String sessionId) { + super(sessionId); + } + + public void setActor(Participant.ActorType actorType, String actorId) { + this.actorType.setValue(actorType); + this.actorId.setValue(actorId); + } + + public void setUserId(String userId) { + this.userId.setValue(userId); + } + + public void setNick(String nick) { + this.nick.setValue(nick); + } + + public void setInternal(Boolean internal) { + this.internal.setValue(internal); + } + + public void setRaisedHand(boolean state, long timestamp) { + this.raisedHand.setValue(new RaisedHand(state, timestamp)); + } + + public void setIceConnectionState(PeerConnection.IceConnectionState iceConnectionState) { + this.iceConnectionState.setValue(iceConnectionState); + } + + public void setMediaStream(MediaStream mediaStream) { + this.mediaStream.setValue(mediaStream); + } + + public void setAudioAvailable(Boolean audioAvailable) { + this.audioAvailable.setValue(audioAvailable); + } + + public void setVideoAvailable(Boolean videoAvailable) { + this.videoAvailable.setValue(videoAvailable); + } + + public void setScreenIceConnectionState(PeerConnection.IceConnectionState screenIceConnectionState) { + this.screenIceConnectionState.setValue(screenIceConnectionState); + } + + public void setScreenMediaStream(MediaStream screenMediaStream) { + this.screenMediaStream.setValue(screenMediaStream); + } + + public void emitReaction(String reaction) { + this.callParticipantModelNotifier.notifyReaction(reaction); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/MutableLocalCallParticipantModel.java b/app/src/main/java/com/nextcloud/talk/call/MutableLocalCallParticipantModel.java new file mode 100644 index 0000000..91bbbfc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/MutableLocalCallParticipantModel.java @@ -0,0 +1,51 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import java.util.Objects; + +/** + * Mutable data model for local call participants. + *

+ * Setting "speaking" will automatically set "speaking" or "speakingWhileMuted" as needed, depending on whether audio is + * enabled or not. Similarly, setting whether the audio is enabled or disabled will automatically switch between + * "speaking" and "speakingWhileMuted" as needed. + *

+ * There is no synchronization when setting the values; if needed, it should be handled by the clients of the model. + */ +public class MutableLocalCallParticipantModel extends LocalCallParticipantModel { + + public void setAudioEnabled(Boolean audioEnabled) { + if (Objects.equals(this.audioEnabled.getValue(), audioEnabled)) { + return; + } + + if (audioEnabled == null || !audioEnabled) { + this.speakingWhileMuted.setValue(this.speaking.getValue()); + this.speaking.setValue(Boolean.FALSE); + } + + this.audioEnabled.setValue(audioEnabled); + + if (audioEnabled != null && audioEnabled) { + this.speaking.setValue(this.speakingWhileMuted.getValue()); + this.speakingWhileMuted.setValue(Boolean.FALSE); + } + } + + public void setSpeaking(Boolean speaking) { + if (this.audioEnabled.getValue() != null && this.audioEnabled.getValue()) { + this.speaking.setValue(speaking); + } else { + this.speakingWhileMuted.setValue(speaking); + } + } + + public void setVideoEnabled(Boolean videoEnabled) { + this.videoEnabled.setValue(videoEnabled); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/RaisedHand.kt b/app/src/main/java/com/nextcloud/talk/call/RaisedHand.kt new file mode 100644 index 0000000..a7cb20e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/RaisedHand.kt @@ -0,0 +1,9 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +data class RaisedHand(val state: Boolean, val timestamp: Long) diff --git a/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt b/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt new file mode 100644 index 0000000..2ae09c6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt @@ -0,0 +1,166 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.view.ViewGroup +import android.view.animation.LinearInterpolator +import android.widget.LinearLayout +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import com.nextcloud.talk.R +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.vanniktech.emoji.EmojiTextView + +class ReactionAnimator( + val context: Context, + private val startPointView: RelativeLayout, + val viewThemeUtils: ViewThemeUtils? +) { + private val reactionsList: MutableList = ArrayList() + + fun addReaction(emoji: String, displayName: String) { + val callReaction = CallReaction(emoji, displayName) + reactionsList.add(callReaction) + + if (reactionsList.size == 1) { + animateReaction(reactionsList[0]) + } + } + + private fun animateReaction(callReaction: CallReaction) { + val reactionWrapper = getReactionWrapperView(callReaction) + + val params = RelativeLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + leftMargin = 0 + bottomMargin = 0 + } + + params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 1) + startPointView.addView(reactionWrapper, params) + + val moveWithFullAlpha = ObjectAnimator.ofFloat( + reactionWrapper, + TRANSLATION_Y_PROPERTY, + POSITION_Y_WITH_FULL_ALPHA + ) + moveWithFullAlpha.duration = DURATION_FULL_ALPHA + moveWithFullAlpha.interpolator = LinearInterpolator() + + val moveWithDecreasingAlpha = ObjectAnimator.ofFloat( + reactionWrapper, + TRANSLATION_Y_PROPERTY, + POSITION_Y_WITH_DECREASING_ALPHA + ) + moveWithDecreasingAlpha.duration = DURATION_DECREASING_ALPHA + moveWithDecreasingAlpha.interpolator = LinearInterpolator() + + val decreasingAlpha: ObjectAnimator = ObjectAnimator.ofFloat( + reactionWrapper, + ALPHA_PROPERTY, + ZERO_ALPHA + ) + decreasingAlpha.duration = DURATION_DECREASING_ALPHA + + val animatorWithFullAlpha = AnimatorSet() + animatorWithFullAlpha.play(moveWithFullAlpha) + + animatorWithFullAlpha.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + reactionsList.remove(callReaction) + if (reactionsList.isNotEmpty()) { + animateReaction(reactionsList[0]) + } + } + }) + + val animatorWithDecreasingAlpha = AnimatorSet() + animatorWithDecreasingAlpha.playTogether(moveWithDecreasingAlpha, decreasingAlpha) + + val finalAnimator = AnimatorSet() + finalAnimator.play(animatorWithFullAlpha).before(animatorWithDecreasingAlpha) + + finalAnimator.start() + } + + private fun getReactionWrapperView(callReaction: CallReaction): LinearLayout { + val reactionWrapper = LinearLayout(context) + reactionWrapper.orientation = LinearLayout.HORIZONTAL + + val emojiView = EmojiTextView(context) + emojiView.text = callReaction.emoji + emojiView.textSize = TEXT_SIZE + + val nameView = getNameView(callReaction) + reactionWrapper.addView(emojiView) + reactionWrapper.addView(nameView) + return reactionWrapper + } + + @SuppressLint("SetTextI18n") + private fun getNameView(callReaction: CallReaction): TextView { + val nameView = TextView(context) + + val nameViewParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + + nameViewParams.setMargins(HORIZONTAL_MARGIN, 0, HORIZONTAL_MARGIN, BOTTOM_MARGIN) + nameView.layoutParams = nameViewParams + + nameView.text = " " + callReaction.userName + " " + nameView.setTextColor(context.resources.getColor(R.color.white, null)) + + val backgroundColor = ContextCompat.getColor( + context, + R.color.colorPrimary + ) + + val drawable = AppCompatResources + .getDrawable(context, R.drawable.reaction_self_background)!! + .mutate() + DrawableCompat.setTintList( + drawable, + ColorStateList.valueOf(backgroundColor) + ) + nameView.background = drawable + return nameView + } + + companion object { + private const val TRANSLATION_Y_PROPERTY = "translationY" + + // 1333ms to move emoji up 400px with full alpha + private const val DURATION_FULL_ALPHA = 1333L + private const val POSITION_Y_WITH_FULL_ALPHA = -400f + + // 666ms to move emoji up 200px while decreasing alpha + private const val DURATION_DECREASING_ALPHA = 666L + private const val POSITION_Y_WITH_DECREASING_ALPHA = -600f + + private const val ZERO_ALPHA = 0f + private const val ALPHA_PROPERTY = "alpha" + + private const val TEXT_SIZE = 20f + private const val HORIZONTAL_MARGIN: Int = 20 + private const val BOTTOM_MARGIN: Int = 5 + } +} +data class CallReaction(var emoji: String, var userName: String) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt new file mode 100644 index 0000000..fa96171 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt @@ -0,0 +1,62 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.call.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import com.nextcloud.talk.adapters.ParticipantUiState + +@Composable +fun AvatarWithFallback(participant: ParticipantUiState, modifier: Modifier = Modifier) { + val initials = participant.nick + .split(" ") + .mapNotNull { it.firstOrNull()?.uppercase() } + .take(2) + .joinToString("") + + Box( + modifier = modifier + .clip(CircleShape), + contentAlignment = Alignment.Center + ) { + if (!participant.avatarUrl.isNullOrEmpty()) { + AsyncImage( + model = participant.avatarUrl, + contentDescription = "Avatar", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + ) + } else { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White, CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + text = initials.ifEmpty { "?" }, + color = Color.Black, + fontSize = 24.sp + ) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt new file mode 100644 index 0000000..d64d628 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -0,0 +1,277 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +@file:Suppress("MagicNumber", "TooManyFunctions") + +package com.nextcloud.talk.call.components + +import android.annotation.SuppressLint +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.adapters.ParticipantUiState +import org.webrtc.EglBase +import kotlin.math.ceil + +@SuppressLint("UnusedBoxWithConstraintsScope") +@Suppress("LongParameterList") +@Composable +fun ParticipantGrid( + modifier: Modifier = Modifier, + eglBase: EglBase?, + participantUiStates: List, + isVoiceOnlyCall: Boolean, + onClick: () -> Unit +) { + val configuration = LocalConfiguration.current + val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT + + val minItemHeight = 100.dp + + if (participantUiStates.isEmpty()) return + + val columns = if (isPortrait) { + when (participantUiStates.size) { + 1, 2, 3 -> 1 + else -> 2 + } + } else { + when (participantUiStates.size) { + 1 -> 1 + 2, 4 -> 2 + else -> 3 + } + }.coerceAtLeast(1) // Prevent 0 + + val rows = ceil(participantUiStates.size / columns.toFloat()).toInt().coerceAtLeast(1) + + val itemSpacing = 8.dp + val edgePadding = 8.dp + val totalVerticalSpacing = itemSpacing * (rows - 1) + val totalVerticalPadding = edgePadding * 2 + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .clickable { onClick() } + ) { + val availableHeight = maxHeight + + val gridAvailableHeight = availableHeight - totalVerticalSpacing - totalVerticalPadding + val rawItemHeight = gridAvailableHeight / rows + val itemHeight = maxOf(rawItemHeight, minItemHeight) + + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + modifier = Modifier + .fillMaxWidth() + .height(availableHeight), + verticalArrangement = Arrangement.spacedBy(itemSpacing), + horizontalArrangement = Arrangement.spacedBy(itemSpacing), + contentPadding = PaddingValues(vertical = edgePadding, horizontal = edgePadding) + ) { + items( + participantUiStates, + key = { it.sessionKey } + ) { participant -> + ParticipantTile( + participantUiState = participant, + modifier = Modifier + .height(itemHeight) + .fillMaxWidth(), + eglBase = eglBase, + isVoiceOnlyCall = isVoiceOnlyCall + ) + } + } + } +} + +@Preview +@Composable +fun ParticipantGridPreview() { + ParticipantGrid( + participantUiStates = getTestParticipants(1), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview +@Composable +fun TwoParticipants() { + ParticipantGrid( + participantUiStates = getTestParticipants(2), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview +@Composable +fun ThreeParticipants() { + ParticipantGrid( + participantUiStates = getTestParticipants(3), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview +@Composable +fun FourParticipants() { + ParticipantGrid( + participantUiStates = getTestParticipants(4), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview +@Composable +fun FiveParticipants() { + ParticipantGrid( + participantUiStates = getTestParticipants(5), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview +@Composable +fun SevenParticipants() { + ParticipantGrid( + participantUiStates = getTestParticipants(7), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview +@Composable +fun FiftyParticipants() { + ParticipantGrid( + participantUiStates = getTestParticipants(50), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun OneParticipantLandscape() { + ParticipantGrid( + participantUiStates = getTestParticipants(1), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun TwoParticipantsLandscape() { + ParticipantGrid( + participantUiStates = getTestParticipants(2), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun ThreeParticipantsLandscape() { + ParticipantGrid( + participantUiStates = getTestParticipants(3), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun FourParticipantsLandscape() { + ParticipantGrid( + participantUiStates = getTestParticipants(4), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun SevenParticipantsLandscape() { + ParticipantGrid( + participantUiStates = getTestParticipants(7), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun FiftyParticipantsLandscape() { + ParticipantGrid( + participantUiStates = getTestParticipants(50), + eglBase = null, + isVoiceOnlyCall = false + ) {} +} + +fun getTestParticipants(numberOfParticipants: Int): List { + val participantList = mutableListOf() + for (i: Int in 1..numberOfParticipants) { + val participant = ParticipantUiState( + sessionKey = i.toString(), + nick = "test$i user", + isConnected = true, + isAudioEnabled = false, + isStreamEnabled = true, + raisedHand = true, + avatarUrl = "", + mediaStream = null + ) + participantList.add(participant) + } + return participantList +} diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt new file mode 100644 index 0000000..ddf9859 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -0,0 +1,146 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.call.components + +import android.annotation.SuppressLint +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.ParticipantUiState +import com.nextcloud.talk.utils.ColorGenerator +import org.webrtc.EglBase + +const val NICK_OFFSET = 4f +const val NICK_BLUR_RADIUS = 4f +const val AVATAR_SIZE_FACTOR = 0.6f + +@SuppressLint("UnusedBoxWithConstraintsScope") +@Suppress("Detekt.LongMethod") +@Composable +fun ParticipantTile( + participantUiState: ParticipantUiState, + eglBase: EglBase?, + modifier: Modifier = Modifier, + isVoiceOnlyCall: Boolean +) { + val colorInt = ColorGenerator.usernameToColor(participantUiState.nick) + + BoxWithConstraints( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(Color(colorInt)) + ) { + val avatarSize = min(maxWidth, maxHeight) * AVATAR_SIZE_FACTOR + + if (!isVoiceOnlyCall && participantUiState.isStreamEnabled && participantUiState.mediaStream != null) { + WebRTCVideoView(participantUiState, eglBase) + } else { + AvatarWithFallback( + participant = participantUiState, + modifier = Modifier + .size(avatarSize) + .align(Alignment.Center) + ) + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(8.dp) + ) { + if (participantUiState.raisedHand) { + Icon( + painter = painterResource(id = R.drawable.ic_hand_back_left), + contentDescription = "Raised Hand", + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(24.dp), + tint = Color.White + ) + } + + if (!participantUiState.isAudioEnabled) { + Icon( + painter = painterResource(id = R.drawable.ic_mic_off_white_24px), + contentDescription = "Mic Off", + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(6.dp) + .size(24.dp), + tint = Color.White + ) + } + + Text( + text = participantUiState.nick, + color = Color.White, + modifier = Modifier + .align(Alignment.BottomStart), + style = MaterialTheme.typography.bodyMedium.copy( + shadow = Shadow( + color = Color.Black, + offset = Offset(NICK_OFFSET, NICK_OFFSET), + blurRadius = NICK_BLUR_RADIUS + ) + ) + ) + + if (!participantUiState.isConnected) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + } + } +} + +@Preview(showBackground = false) +@Composable +fun ParticipantTilePreview() { + val participant = ParticipantUiState( + sessionKey = "", + nick = "testuser one", + isConnected = true, + isAudioEnabled = false, + isStreamEnabled = true, + raisedHand = true, + avatarUrl = "", + mediaStream = null + ) + ParticipantTile( + participantUiState = participant, + modifier = Modifier + .fillMaxWidth() + .height(300.dp), + eglBase = null, + isVoiceOnlyCall = false + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt new file mode 100644 index 0000000..3756274 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.call.components + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.nextcloud.talk.adapters.ParticipantUiState +import org.webrtc.EglBase +import org.webrtc.SurfaceViewRenderer + +@Composable +fun WebRTCVideoView(participant: ParticipantUiState, eglBase: EglBase?) { + AndroidView( + factory = { context -> + SurfaceViewRenderer(context).apply { + init(eglBase?.eglBaseContext, null) + setEnableHardwareScaler(true) + setMirror(false) + participant.mediaStream?.videoTracks?.firstOrNull()?.addSink(this) + } + }, + modifier = Modifier.fillMaxSize(), + onRelease = { + participant.mediaStream?.videoTracks?.firstOrNull()?.removeSink(it) + it.release() + } + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java new file mode 100644 index 0000000..d659278 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java @@ -0,0 +1,91 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.callbacks; + +import android.content.Context; +import android.text.Editable; +import android.text.Spanned; +import android.widget.EditText; + +import com.nextcloud.talk.R; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.models.json.mention.Mention; +import com.nextcloud.talk.ui.theme.ViewThemeUtils; +import com.nextcloud.talk.utils.DisplayUtils; +import com.nextcloud.talk.utils.CharPolicy; +import com.nextcloud.talk.utils.text.Spans; +import com.otaliastudios.autocomplete.AutocompleteCallback; +import com.vanniktech.emoji.EmojiRange; +import com.vanniktech.emoji.Emojis; + +import java.util.Objects; + +import kotlin.OptIn; +import third.parties.fresco.BetterImageSpan; + +public class MentionAutocompleteCallback implements AutocompleteCallback { + private final ViewThemeUtils viewThemeUtils; + private Context context; + private User conversationUser; + private EditText editText; + + public MentionAutocompleteCallback(Context context, + User conversationUser, + EditText editText, + ViewThemeUtils viewThemeUtils) { + this.context = context; + this.conversationUser = conversationUser; + this.editText = editText; + this.viewThemeUtils = viewThemeUtils; + } + + @OptIn(markerClass = kotlin.ExperimentalStdlibApi.class) + @Override + public boolean onPopupItemClicked(Editable editable, Mention item) { + CharPolicy.TextSpan range = CharPolicy.getQueryRange(editable); + if (range == null) { + return false; + } + String replacement = item.getLabel(); + + StringBuilder replacementStringBuilder = new StringBuilder(Objects.requireNonNull(item.getLabel())); + for (EmojiRange emojiRange : Emojis.emojis(replacement)) { + replacementStringBuilder.delete(emojiRange.range.getStart(), emojiRange.range.getEndInclusive()); + } + + String charSequence = " "; + editable.replace(range.getStart(), range.getEnd(), charSequence + replacementStringBuilder + " "); + String id; + if (item.getMentionId() != null) id = item.getMentionId(); else id = item.getId(); + Spans.MentionChipSpan mentionChipSpan = + new Spans.MentionChipSpan(DisplayUtils.getDrawableForMentionChipSpan(context, + item.getId(), + item.getRoomToken(), + item.getLabel(), + conversationUser, + item.getSource(), + R.xml.chip_you, + editText, + viewThemeUtils, + "federated_users".equals(item.getSource())), + BetterImageSpan.ALIGN_CENTER, + id, item.getLabel()); + editable.setSpan(mentionChipSpan, + range.getStart() + charSequence.length(), + range.getStart() + replacementStringBuilder.length() + charSequence.length(), + Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + + return true; + } + + @Override + public void onPopupVisibilityChanged(boolean shown) { + + } +} diff --git a/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt new file mode 100644 index 0000000..aaad05b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/callnotification/CallNotificationActivity.kt @@ -0,0 +1,244 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.callnotification + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.res.Configuration +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.View +import androidx.core.app.NotificationManagerCompat +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.CallActivity +import com.nextcloud.talk.activities.CallBaseActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.CallNotificationActivityBinding +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ONE_TO_ONE +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import okhttp3.Cache +import java.io.IOException +import javax.inject.Inject + +@SuppressLint("LongLogTag") +@AutoInjector(NextcloudTalkApplication::class) +class CallNotificationActivity : CallBaseActivity() { + @JvmField + @Inject + var ncApi: NcApi? = null + + @JvmField + @Inject + var cache: Cache? = null + + @Inject + lateinit var userManager: UserManager + + private var roomToken: String? = null + private var notificationTimestamp: Int? = null + private var displayName: String? = null + private var callFlag: Int = 0 + private var isOneToOneCall: Boolean = true + private var conversationName: String? = null + private var internalUserId: Long = -1 + + private var userBeingCalled: User? = null + private var leavingScreen = false + private var handler: Handler? = null + private var binding: CallNotificationActivityBinding? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + binding = CallNotificationActivityBinding.inflate(layoutInflater) + setContentView(binding!!.root) + hideNavigationIfNoPipAvailable() + + handleExtras() + userBeingCalled = userManager.getUserWithId(internalUserId).blockingGet() + + setupCallTypeDescription() + binding!!.conversationNameTextView.text = displayName + setupAvatar(isOneToOneCall, conversationName) + initClickListeners() + setupNotificationCanceledRoutine() + } + + private fun handleExtras() { + val extras = intent.extras!! + roomToken = extras.getString(KEY_ROOM_TOKEN, "") + notificationTimestamp = extras.getInt(BundleKeys.KEY_NOTIFICATION_TIMESTAMP) + displayName = extras.getString(BundleKeys.KEY_CONVERSATION_DISPLAY_NAME, "") + callFlag = extras.getInt(BundleKeys.KEY_CALL_FLAG) + isOneToOneCall = extras.getBoolean(KEY_ROOM_ONE_TO_ONE) + conversationName = extras.getString(BundleKeys.KEY_CONVERSATION_NAME, "") + internalUserId = extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) + } + + private fun setupAvatar(isOneToOneCall: Boolean, conversationName: String?) { + if (isOneToOneCall) { + binding!!.avatarImageView.loadUserAvatar( + userBeingCalled!!, + conversationName!!, + true, + false + ) + } else { + binding!!.avatarImageView.setImageResource(R.drawable.ic_circular_group) + } + } + + private fun setupCallTypeDescription() { + val apiVersion = ApiUtils.getConversationApiVersion( + userBeingCalled!!, + intArrayOf( + ApiUtils.API_V4, + ApiUtils.API_V3, + 1 + ) + ) + + if (apiVersion >= ApiUtils.API_V3) { + val hasCallFlags = hasSpreedFeatureCapability( + userBeingCalled?.capabilities?.spreedCapability!!, + SpreedFeatures.CONVERSATION_CALL_FLAGS + ) + if (hasCallFlags) { + if (isInCallWithVideo(callFlag)) { + binding!!.incomingCallVoiceOrVideoTextView.text = String.format( + resources.getString(R.string.nc_call_video), + resources.getString(R.string.nc_app_product_name) + ) + } else { + binding!!.incomingCallVoiceOrVideoTextView.text = String.format( + resources.getString(R.string.nc_call_voice), + resources.getString(R.string.nc_app_product_name) + ) + } + } + } else { + val callDescriptionWithoutTypeInfo = String.format( + resources.getString(R.string.nc_call_unknown), + resources.getString(R.string.nc_app_product_name) + ) + binding!!.incomingCallVoiceOrVideoTextView.text = callDescriptionWithoutTypeInfo + } + } + + private fun setupNotificationCanceledRoutine() { + val notificationHandler = Handler(Looper.getMainLooper()) + notificationHandler.post(object : Runnable { + override fun run() { + if (NotificationUtils.isNotificationVisible(context, notificationTimestamp!!.toInt())) { + notificationHandler.postDelayed(this, ONE_SECOND) + } else { + finish() + } + } + }) + } + + override fun onStart() { + super.onStart() + if (handler == null) { + handler = Handler() + try { + cache!!.evictAll() + } catch (e: IOException) { + Log.e(TAG, "Failed to evict cache") + } + } + } + + private fun initClickListeners() { + binding!!.callAnswerVoiceOnlyView.setOnClickListener { + Log.d(TAG, "accept call (voice only)") + intent.putExtra(KEY_CALL_VOICE_ONLY, true) + proceedToCall() + } + binding!!.callAnswerCameraView.setOnClickListener { + Log.d(TAG, "accept call (with video)") + intent.putExtra(KEY_CALL_VOICE_ONLY, false) + proceedToCall() + } + binding!!.hangupButton.setOnClickListener { hangup() } + } + + private fun hangup() { + leavingScreen = true + finish() + } + + private fun proceedToCall() { + val callIntent = Intent(this, CallActivity::class.java) + intent.putExtra(KEY_ROOM_ONE_TO_ONE, isOneToOneCall) + callIntent.putExtras(intent.extras!!) + startActivity(callIntent) + } + + private fun isInCallWithVideo(callFlag: Int): Boolean = (callFlag and Participant.InCallFlags.WITH_VIDEO) > 0 + + override fun onStop() { + val notificationManager = NotificationManagerCompat.from(context) + notificationManager.cancel(notificationTimestamp!!) + super.onStop() + } + + public override fun onDestroy() { + leavingScreen = true + if (handler != null) { + handler!!.removeCallbacksAndMessages(null) + handler = null + } + super.onDestroy() + } + + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + isInPipMode = isInPictureInPictureMode + if (isInPictureInPictureMode) { + updateUiForPipMode() + } else { + updateUiForNormalMode() + } + } + + override fun updateUiForPipMode() { + binding!!.callAnswerButtons.visibility = View.INVISIBLE + binding!!.incomingCallRelativeLayout.visibility = View.INVISIBLE + } + + override fun updateUiForNormalMode() { + binding!!.callAnswerButtons.visibility = View.VISIBLE + binding!!.incomingCallRelativeLayout.visibility = View.VISIBLE + } + + override fun suppressFitsSystemWindows() { + binding!!.callNotificationLayout.fitsSystemWindows = false + } + + companion object { + private val TAG = CallNotificationActivity::class.simpleName + const val ONE_SECOND: Long = 1000 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt new file mode 100644 index 0000000..6477727 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -0,0 +1,4602 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2024 Parneet Singh + * SPDX-FileCopyrightText: 2024 Giacomo Pacini + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2021-2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2021-2022 Tim Krüger + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat + +import android.Manifest +import android.annotation.SuppressLint +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.AssetFileDescriptor +import android.database.Cursor +import android.graphics.drawable.Drawable +import android.location.LocationManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.provider.ContactsContract +import android.provider.MediaStore +import android.provider.Settings +import android.text.SpannableStringBuilder +import android.text.TextUtils +import android.util.Log +import android.view.Gravity +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.AbsListView +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.PopupMenu +import android.widget.PopupWindow +import android.widget.TextView +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.view.ContextThemeWrapper +import androidx.cardview.widget.CardView +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ComposeView +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.FileProvider +import androidx.core.content.PermissionChecker +import androidx.core.content.PermissionChecker.PERMISSION_GRANTED +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.drawable.toDrawable +import androidx.core.net.toUri +import androidx.core.os.bundleOf +import androidx.core.text.bold +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.emoji2.text.EmojiCompat +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.commit +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import coil.imageLoader +import coil.load +import coil.request.CachePolicy +import coil.request.ImageRequest +import coil.target.Target +import coil.transform.CircleCropTransformation +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.color.ColorUtil +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.activities.CallActivity +import com.nextcloud.talk.activities.TakePhotoActivity +import com.nextcloud.talk.adapters.messages.CallStartedMessageInterface +import com.nextcloud.talk.adapters.messages.CommonMessageInterface +import com.nextcloud.talk.adapters.messages.IncomingDeckCardViewHolder +import com.nextcloud.talk.adapters.messages.IncomingLinkPreviewMessageViewHolder +import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder +import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder +import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder +import com.nextcloud.talk.adapters.messages.IncomingTextMessageViewHolder +import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder +import com.nextcloud.talk.adapters.messages.MessagePayload +import com.nextcloud.talk.adapters.messages.OutcomingDeckCardViewHolder +import com.nextcloud.talk.adapters.messages.OutcomingLinkPreviewMessageViewHolder +import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder +import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder +import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder +import com.nextcloud.talk.adapters.messages.OutcomingTextMessageViewHolder +import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder +import com.nextcloud.talk.adapters.messages.PreviewMessageInterface +import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder +import com.nextcloud.talk.adapters.messages.SystemMessageInterface +import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder +import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter +import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder +import com.nextcloud.talk.adapters.messages.VoiceMessageInterface +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel +import com.nextcloud.talk.conversationinfo.ConversationInfoActivity +import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel +import com.nextcloud.talk.conversationlist.ConversationsListActivity +import com.nextcloud.talk.data.network.NetworkMonitor +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityChatBinding +import com.nextcloud.talk.events.UserMentionClickEvent +import com.nextcloud.talk.events.WebSocketCommunicationEvent +import com.nextcloud.talk.extensions.loadAvatarOrImagePreview +import com.nextcloud.talk.jobs.DeleteConversationWorker +import com.nextcloud.talk.jobs.DownloadFileToCacheWorker +import com.nextcloud.talk.jobs.ShareOperationWorker +import com.nextcloud.talk.jobs.UploadAndShareFilesWorker +import com.nextcloud.talk.location.LocationPickerActivity +import com.nextcloud.talk.messagesearch.MessageSearchActivity +import com.nextcloud.talk.models.ExternalSignalingServer +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall +import com.nextcloud.talk.models.json.threads.ThreadInfo +import com.nextcloud.talk.polls.ui.PollCreateDialogFragment +import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity +import com.nextcloud.talk.shareditems.activities.SharedItemsActivity +import com.nextcloud.talk.signaling.SignalingMessageReceiver +import com.nextcloud.talk.signaling.SignalingMessageSender +import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity +import com.nextcloud.talk.translate.ui.TranslateActivity +import com.nextcloud.talk.ui.PlaybackSpeed +import com.nextcloud.talk.ui.PlaybackSpeedControl +import com.nextcloud.talk.ui.StatusDrawable +import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet +import com.nextcloud.talk.ui.dialog.ContextChatCompose +import com.nextcloud.talk.ui.dialog.DateTimeCompose +import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment +import com.nextcloud.talk.ui.dialog.MessageActionsDialog +import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment +import com.nextcloud.talk.ui.dialog.ShowReactionsDialog +import com.nextcloud.talk.ui.dialog.TempMessageActionsDialog +import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions +import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.AudioUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfEventRooms +import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfInstantMeetingRoom +import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfSIPRoom +import com.nextcloud.talk.utils.ContactUtils +import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.DateConstants +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.FileUtils +import com.nextcloud.talk.utils.FileViewerUtils +import com.nextcloud.talk.utils.Mimetype +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.ParticipantPermissions +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.VibrationUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_RECORDING_STATE +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil +import com.nextcloud.talk.utils.rx.DisposableSet +import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder +import com.nextcloud.talk.webrtc.WebSocketConnectionHelper +import com.nextcloud.talk.webrtc.WebSocketInstance +import com.otaliastudios.autocomplete.Autocomplete +import com.stfalcon.chatkit.commons.ImageLoader +import com.stfalcon.chatkit.commons.models.IMessage +import com.stfalcon.chatkit.messages.MessageHolders +import com.stfalcon.chatkit.messages.MessageHolders.ContentChecker +import com.stfalcon.chatkit.messages.MessagesListAdapter +import com.stfalcon.chatkit.utils.DateFormatter +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.io.File +import java.io.IOException +import java.net.HttpURLConnection +import java.text.SimpleDateFormat +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.Date +import java.util.Locale +import java.util.concurrent.ExecutionException +import javax.inject.Inject +import kotlin.math.roundToInt +import androidx.core.content.ContextCompat + +@Suppress("TooManyFunctions") +@AutoInjector(NextcloudTalkApplication::class) +class ChatActivity : + BaseActivity(), + MessagesListAdapter.OnLoadMoreListener, + MessagesListAdapter.Formatter, + MessagesListAdapter.OnMessageViewLongClickListener, + ContentChecker, + VoiceMessageInterface, + CommonMessageInterface, + PreviewMessageInterface, + SystemMessageInterface, + CallStartedMessageInterface { + + var active = false + + private lateinit var binding: ActivityChatBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var permissionUtil: PlatformPermissionUtil + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var colorUtil: ColorUtil + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var networkMonitor: NetworkMonitor + + lateinit var chatViewModel: ChatViewModel + + lateinit var conversationInfoViewModel: ConversationInfoViewModel + lateinit var messageInputViewModel: MessageInputViewModel + + private var chatMenu: Menu? = null + + private var overflowMenuHostView: ComposeView? = null + private var isThreadMenuExpanded by mutableStateOf(false) + + private val startSelectContactForResult = registerForActivityResult( + ActivityResultContracts + .StartActivityForResult() + ) { + executeIfResultOk(it) { intent -> + onSelectContactResult(intent) + } + } + + private val startChooseFileIntentForResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + executeIfResultOk(it) { intent -> + onChooseFileResult(intent) + } + } + + private val startRemoteFileBrowsingForResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + executeIfResultOk(it) { intent -> + onRemoteFileBrowsingResult(intent) + } + } + + private val startMessageSearchForResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + executeIfResultOk(it) { intent -> + runBlocking { + val id = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID) + id?.let { + startContextChatWindowForMessage(id) + } + } + } + } + + private fun startContextChatWindowForMessage(id: String?) { + binding.genericComposeView.apply { + val shouldDismiss = mutableStateOf(false) + setContent { + val bundle = bundleOf() + bundle.putString(BundleKeys.KEY_CREDENTIALS, credentials!!) + bundle.putString(BundleKeys.KEY_BASE_URL, conversationUser!!.baseUrl) + bundle.putString(KEY_ROOM_TOKEN, roomToken) + bundle.putString(BundleKeys.KEY_MESSAGE_ID, id) + bundle.putString( + KEY_CONVERSATION_NAME, + currentConversation!!.displayName + ) + ContextChatCompose(bundle).GetDialogView(shouldDismiss, context) + } + } + Log.d(TAG, "Should open something else") + } + + private val startPickCameraIntentForResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + executeIfResultOk(it) { intent -> + onPickCameraResult(intent) + } + } + + override val view: View + get() = binding.root + + val disposables = DisposableSet() + + var sessionIdAfterRoomJoined: String? = null + lateinit var roomToken: String + var conversationThreadId: Long? = null + var conversationThreadInfo: ThreadInfo? = null + var conversationUser: User? = null + lateinit var spreedCapabilities: SpreedCapability + var chatApiVersion: Int = 1 + private var roomPassword: String = "" + var credentials: String? = null + var currentConversation: ConversationModel? = null + var adapter: TalkMessagesListAdapter? = null + var mentionAutocomplete: Autocomplete<*>? = null + var layoutManager: LinearLayoutManager? = null + var pullChatMessagesPending = false + var startCallFromNotification: Boolean = false + var startCallFromRoomSwitch: Boolean = false + + var voiceOnly: Boolean = true + var focusInput: Boolean = false + private lateinit var path: String + + var myFirstMessage: CharSequence? = null + var checkingLobbyStatus: Boolean = false + + private var conversationVoiceCallMenuItem: MenuItem? = null + private var conversationVideoMenuItem: MenuItem? = null + private var eventConversationMenuItem: MenuItem? = null + + var webSocketInstance: WebSocketInstance? = null + var signalingMessageSender: SignalingMessageSender? = null + var externalSignalingServer: ExternalSignalingServer? = null + + var getRoomInfoTimerHandler: Handler? = null + + private val filesToUpload: MutableList = ArrayList() + lateinit var sharedText: String + + lateinit var participantPermissions: ParticipantPermissions + + private var videoURI: Uri? = null + + private lateinit var pickMultipleMedia: ActivityResultLauncher + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (isChatThread()) { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } else { + val intent = Intent(this@ChatActivity, ConversationsListActivity::class.java) + intent.putExtras(Bundle()) + startActivity(intent) + } + } + } + + private lateinit var messageInputFragment: MessageInputFragment + + val typingParticipants = HashMap() + + var callStarted = false + + private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener { + override fun onSwitchTo(token: String?) { + if (token != null) { + if (CallActivity.active) { + Log.d(TAG, "CallActivity is running. Ignore to switch chat in ChatActivity...") + } else { + switchToRoom(token, false, false) + } + } + } + } + + private val conversationMessageListener = object : SignalingMessageReceiver.ConversationMessageListener { + override fun onStartTyping(userId: String?, session: String?) { + val userIdOrGuestSession = userId ?: session + + if (isTypingStatusEnabled() && conversationUser?.userId != userIdOrGuestSession) { + var displayName = webSocketInstance?.getDisplayNameForSession(session) + + if (displayName != null && !typingParticipants.contains(userIdOrGuestSession)) { + if (displayName == "") { + displayName = context.resources?.getString(R.string.nc_guest)!! + } + + runOnUiThread { + val typingParticipant = TypingParticipant(userIdOrGuestSession!!, displayName) { + typingParticipants.remove(userIdOrGuestSession) + updateTypingIndicator() + } + + typingParticipants[userIdOrGuestSession] = typingParticipant + updateTypingIndicator() + } + } else if (typingParticipants.contains(userIdOrGuestSession)) { + typingParticipants[userIdOrGuestSession]?.restartTimer() + } + } + } + + override fun onStopTyping(userId: String?, session: String?) { + val userIdOrGuestSession = userId ?: session + + if (isTypingStatusEnabled() && conversationUser?.userId != userId) { + typingParticipants[userIdOrGuestSession]?.cancelTimer() + typingParticipants.remove(userIdOrGuestSession) + updateTypingIndicator() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityChatBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + ViewCompat.setOnApplyWindowInsetsListener(binding.chatContainer) { view, insets -> + val systemBarInsets = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + + val isKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) + val bottomPadding = if (isKeyboardVisible) imeInsets.bottom else systemBarInsets.bottom + + view.setPadding( + systemBarInsets.left, + systemBarInsets.top, + systemBarInsets.right, + bottomPadding + ) + WindowInsetsCompat.CONSUMED + } + } else { + colorizeStatusBar() + colorizeNavigationBar() + } + + conversationUser = currentUserProvider.currentUser.blockingGet() + handleIntent(intent) + + chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java] + + conversationInfoViewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java] + + val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken) + val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token) + chatViewModel.initData( + credentials!!, + urlForChatting, + roomToken, + conversationThreadId + ) + + conversationThreadId?.let { + val threadUrl = ApiUtils.getUrlForThread( + version = 1, + baseUrl = conversationUser!!.baseUrl, + token = roomToken, + threadId = it.toInt() + ) + chatViewModel.getThread(credentials, threadUrl) + } + + messageInputFragment = getMessageInputFragment() + messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java] + messageInputViewModel.setData(chatViewModel.getChatRepository()) + + binding.progressBar.visibility = View.VISIBLE + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + + initObservers() + + pickMultipleMedia = registerForActivityResult( + ActivityResultContracts.PickMultipleVisualMedia(MAX_AMOUNT_MEDIA_FILE_PICKER) + ) { uris -> + if (uris.isNotEmpty()) { + onChooseFileResult(uris) + } + } + } + + private fun getMessageInputFragment(): MessageInputFragment { + val internalId = conversationUser!!.id.toString() + "@" + roomToken + return MessageInputFragment().apply { + arguments = Bundle().apply { + putString(CONVERSATION_INTERNAL_ID, internalId) + putString(BundleKeys.KEY_SHARED_TEXT, sharedText) + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + val extras: Bundle? = intent.extras + + val requestedRoomSwitch = extras?.getBoolean(KEY_SWITCH_TO_ROOM, false) == true + + if (requestedRoomSwitch) { + val newRoomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty() + val startCallAfterRoomSwitch = extras?.getBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, false) == true + val isVoiceOnlyCall = extras?.getBoolean(KEY_CALL_VOICE_ONLY, false) == true + + if (newRoomToken != roomToken) { + switchToRoom(newRoomToken, startCallAfterRoomSwitch, isVoiceOnlyCall) + } + } else { + handleIntent(intent) + } + } + + private fun handleIntent(intent: Intent) { + val extras: Bundle? = intent.extras + + roomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty() + + conversationThreadId = if (extras?.containsKey(KEY_THREAD_ID) == true) { + extras.getLong(KEY_THREAD_ID) + } else { + null + } + + sharedText = extras?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty() + + Log.d(TAG, " roomToken = $roomToken") + if (roomToken.isEmpty()) { + Log.d(TAG, " roomToken was null or empty!") + } + + roomPassword = extras?.getString(BundleKeys.KEY_CONVERSATION_PASSWORD).orEmpty() + + credentials = if (conversationUser?.userId == "?") { + null + } else { + ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token) + } + + startCallFromNotification = extras?.getBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false) == true + startCallFromRoomSwitch = extras?.getBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, false) == true + + voiceOnly = extras?.getBoolean(KEY_CALL_VOICE_ONLY, false) == true + + focusInput = extras?.getBoolean(BundleKeys.KEY_FOCUS_INPUT) == true + } + + override fun onStart() { + super.onStart() + active = true + this.lifecycle.addObserver(AudioUtils) + this.lifecycle.addObserver(chatViewModel) + } + + override fun onSaveInstanceState(outState: Bundle) { + chatViewModel.handleOrientationChange() + super.onSaveInstanceState(outState) + } + + override fun onStop() { + super.onStop() + active = false + adapter = null + this.lifecycle.removeObserver(AudioUtils) + this.lifecycle.removeObserver(chatViewModel) + } + + @SuppressLint("NotifyDataSetChanged", "SetTextI18n", "ResourceAsColor") + @Suppress("LongMethod") + private fun initObservers() { + Log.d(TAG, "initObservers Called") + + this.lifecycleScope.launch { + chatViewModel.getConversationFlow + .onEach { conversationModel -> + currentConversation = conversationModel + chatViewModel.updateConversation( + currentConversation!! + ) + + logConversationInfos("GetRoomSuccessState") + + if (adapter == null) { + initAdapter() + binding.messagesListView.setAdapter(adapter) + layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager? + } + + chatViewModel.getCapabilities(conversationUser!!, roomToken, currentConversation!!) + }.collect() + } + + chatViewModel.getRoomViewState.observe(this) { state -> + when (state) { + is ChatViewModel.GetRoomSuccessState -> { + // unused atm + } + + is ChatViewModel.GetRoomErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + + chatViewModel.getCapabilitiesViewState.observe(this) { state -> + when (state) { + is ChatViewModel.GetCapabilitiesUpdateState -> { + if (currentConversation != null) { + spreedCapabilities = state.spreedCapabilities + chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1)) + participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!) + + invalidateOptionsMenu() + isEventConversation() + checkShowCallButtons() + checkLobbyState() + updateRoomTimerHandler() + } else { + Log.w( + TAG, + "currentConversation was null in observer ChatViewModel.GetCapabilitiesUpdateState" + ) + } + } + + is ChatViewModel.GetCapabilitiesInitialLoadState -> { + if (currentConversation != null) { + spreedCapabilities = state.spreedCapabilities + chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1)) + participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!) + + supportFragmentManager.commit { + setReorderingAllowed(true) // optimizes out redundant replace operations + replace(R.id.fragment_container_activity_chat, messageInputFragment) + runOnCommit { + if (focusInput) { + messageInputFragment.binding.fragmentMessageInputView.requestFocus() + } + } + } + + joinRoomWithPassword() + + if (conversationUser?.userId != "?" && + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG) && + !isChatThread() + ) { + binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() } + } + + loadAvatarForStatusBar() + setupSwipeToReply() + setActionBarTitle() + isEventConversation() + checkShowCallButtons() + checkLobbyState() + if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && + currentConversation?.status == "dnd" + ) { + conversationUser?.let { user -> + val credentials = ApiUtils.getCredentials(user.username, user.token) + chatViewModel.outOfOfficeStatusOfUser( + credentials!!, + user.baseUrl!!, + currentConversation!!.name + ) + } + } + + if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT && + hasSpreedFeatureCapability( + conversationUser?.capabilities!!.spreedCapability!!, + SpreedFeatures.UNBIND_CONVERSATION + ) + ) { + val eventEndTimeStamp = + currentConversation?.objectId + ?.split("#") + ?.getOrNull(1) + ?.toLongOrNull() + val currentTimeStamp = (System.currentTimeMillis() / ONE_SECOND_IN_MILLIS).toLong() + val retentionPeriod = retentionOfEventRooms(spreedCapabilities) + val isPastEvent = eventEndTimeStamp?.let { it < currentTimeStamp } + if (isPastEvent == true && retentionPeriod != 0) { + showConversationDeletionWarning(retentionPeriod) + } + } + + if (currentConversation?.objectType == ConversationEnums.ObjectType.PHONE_TEMPORARY && + hasSpreedFeatureCapability( + conversationUser?.capabilities!!.spreedCapability!!, + SpreedFeatures.UNBIND_CONVERSATION + ) + ) { + val retentionPeriod = retentionOfSIPRoom(spreedCapabilities) + val systemMessage = currentConversation?.lastMessage?.systemMessageType + if (retentionPeriod != 0 && + ( + systemMessage == ChatMessage.SystemMessageType.CALL_ENDED || + systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE + ) + ) { + showConversationDeletionWarning(retentionPeriod) + } + } + + if (currentConversation?.objectType == ConversationEnums.ObjectType.INSTANT_MEETING && + hasSpreedFeatureCapability( + conversationUser?.capabilities!!.spreedCapability!!, + SpreedFeatures.UNBIND_CONVERSATION + ) + ) { + val retentionPeriod = retentionOfInstantMeetingRoom(spreedCapabilities) + val systemMessage = currentConversation?.lastMessage?.systemMessageType + if (retentionPeriod != 0 && + ( + systemMessage == ChatMessage.SystemMessageType.CALL_ENDED || + systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE + ) + ) { + showConversationDeletionWarning(retentionPeriod) + } + } + + updateRoomTimerHandler(MILLIS_250) + + val urlForChatting = + ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken) + + chatViewModel.loadMessages( + withCredentials = credentials!!, + withUrl = urlForChatting + ) + } else { + Log.w( + TAG, + "currentConversation was null in observer ChatViewModel.GetCapabilitiesInitialLoadState" + ) + } + } + + is ChatViewModel.GetCapabilitiesErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + + chatViewModel.joinRoomViewState.observe(this) { state -> + when (state) { + is ChatViewModel.JoinRoomSuccessState -> { + currentConversation = state.conversationModel + + sessionIdAfterRoomJoined = currentConversation!!.sessionId + ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation!!.sessionId + ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = currentConversation!!.token + ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser + + logConversationInfos("joinRoomWithPassword#onNext") + + setupWebsocket() + + if (startCallFromNotification) { + startCallFromNotification = false + startACall(voiceOnly, false) + } + + if (startCallFromRoomSwitch) { + startCallFromRoomSwitch = false + startACall(voiceOnly, true) + } + } + + is ChatViewModel.JoinRoomErrorState -> {} + + else -> {} + } + } + + chatViewModel.leaveRoomViewState.observe(this) { state -> + when (state) { + is ChatViewModel.LeaveRoomSuccessState -> { + logConversationInfos("leaveRoom#onNext") + + checkingLobbyStatus = false + + if (getRoomInfoTimerHandler != null) { + getRoomInfoTimerHandler?.removeCallbacksAndMessages(null) + } + + if (webSocketInstance != null && currentConversation != null) { + webSocketInstance?.joinRoomWithRoomTokenAndSession( + "", + sessionIdAfterRoomJoined + ) + } + + sessionIdAfterRoomJoined = "0" + + if (state.funToCallWhenLeaveSuccessful != null) { + Log.d(TAG, "a callback action was set and is now executed because room was left successfully") + state.funToCallWhenLeaveSuccessful.invoke() + } + } + + else -> {} + } + } + + messageInputViewModel.sendChatMessageViewState.observe(this) { state -> + when (state) { + is MessageInputViewModel.SendChatMessageSuccessState -> { + myFirstMessage = state.message + + removeUnreadMessagesMarker() + + if (binding.unreadMessagesPopup.isShown) { + binding.unreadMessagesPopup.visibility = View.GONE + } + binding.messagesListView.smoothScrollToPosition(0) + } + + is MessageInputViewModel.SendChatMessageErrorState -> { + binding.messagesListView.smoothScrollToPosition(0) + } + + else -> {} + } + } + + chatViewModel.deleteChatMessageViewState.observe(this) { state -> + when (state) { + is ChatViewModel.DeleteChatMessageSuccessState -> { + if (state.msg.ocs!!.meta!!.statusCode == HttpURLConnection.HTTP_ACCEPTED) { + Snackbar.make( + binding.root, + R.string.nc_delete_message_leaked_to_matterbridge, + Snackbar.LENGTH_LONG + ).show() + } + + val id = state.msg.ocs!!.data!!.parentMessage!!.id.toString() + val index = adapter?.getMessagePositionById(id) ?: 0 + val message = adapter?.items?.get(index)?.item as ChatMessage + setMessageAsDeleted(message) + } + + is ChatViewModel.DeleteChatMessageErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + + chatViewModel.createRoomViewState.observe(this) { state -> + when (state) { + is ChatViewModel.CreateRoomSuccessState -> { + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, state.roomOverall.ocs!!.data!!.token) + + leaveRoom { + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(chatIntent) + } + } + + is ChatViewModel.CreateRoomErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + + chatViewModel.chatMessageViewState.observe(this) { state -> + when (state) { + is ChatViewModel.ChatMessageStartState -> { + // Handle UI on first load + cancelNotificationsForCurrentConversation() + binding.progressBar.visibility = View.GONE + binding.offline.root.visibility = View.GONE + binding.messagesListView.visibility = View.VISIBLE + collapseSystemMessages() + } + + is ChatViewModel.ChatMessageUpdateState -> { + binding.progressBar.visibility = View.GONE + binding.offline.root.visibility = View.GONE + binding.messagesListView.visibility = View.VISIBLE + } + + is ChatViewModel.ChatMessageErrorState -> { + // unused atm + } + + else -> {} + } + } + + this.lifecycleScope.launch { + chatViewModel.getMessageFlow + .onEach { triple -> + val lookIntoFuture = triple.first + val setUnreadMessagesMarker = triple.second + var chatMessageList = triple.third + + chatMessageList = handleSystemMessages(chatMessageList) + chatMessageList = handleThreadMessages(chatMessageList) + if (chatMessageList.isEmpty()) { + return@onEach + } + + determinePreviousMessageIds(chatMessageList) + + handleExpandableSystemMessages(chatMessageList) + + if (ChatMessage.SystemMessageType.CLEARED_CHAT == chatMessageList[0].systemMessageType) { + adapter?.clear() + adapter?.notifyDataSetChanged() + } + + if (lookIntoFuture) { + Log.d(TAG, "chatMessageList.size in getMessageFlow:" + chatMessageList.size) + processMessagesFromTheFuture(chatMessageList, setUnreadMessagesMarker) + } else { + processMessagesNotFromTheFuture(chatMessageList) + collapseSystemMessages() + } + + processExpiredMessages() + processCallStartedMessages() + + adapter?.notifyDataSetChanged() + } + .collect() + } + + this.lifecycleScope.launch { + chatViewModel.getRemoveMessageFlow + .onEach { + removeMessageById(it.id) + } + .collect() + } + + this.lifecycleScope.launch { + chatViewModel.getUpdateMessageFlow + .onEach { + updateMessageInsideAdapter(it) + } + .collect() + } + + this.lifecycleScope.launch { + chatViewModel.getLastCommonReadFlow + .onEach { + updateReadStatusOfAllMessages(it) + processExpiredMessages() + } + .collect() + } + + this.lifecycleScope.launch { + chatViewModel.getLastReadMessageFlow + .onEach { lastRead -> + scrollToAndCenterMessageWithId(lastRead.toString()) + } + .collect() + } + + this.lifecycleScope.launch { + chatViewModel.getGeneralUIFlow.onEach { key -> + when (key) { + NO_OFFLINE_MESSAGES_FOUND -> { + binding.progressBar.visibility = View.GONE + binding.messagesListView.visibility = View.GONE + binding.offline.root.visibility = View.VISIBLE + } + + else -> {} + } + }.collect() + } + + this.lifecycleScope.launch { + chatViewModel.mediaPlayerSeekbarObserver.onEach { msg -> + adapter?.update(msg) + }.collect() + } + + chatViewModel.reactionDeletedViewState.observe(this) { state -> + when (state) { + is ChatViewModel.ReactionDeletedSuccessState -> { + updateUiToDeleteReaction( + state.reactionDeletedModel.chatMessage, + state.reactionDeletedModel.emoji + ) + } + + else -> {} + } + } + + chatViewModel.reactionAddedViewState.observe(this) { state -> + when (state) { + is ChatViewModel.ReactionAddedSuccessState -> { + updateUiToAddReaction( + state.reactionAddedModel.chatMessage, + state.reactionAddedModel.emoji + ) + } + + else -> {} + } + } + + messageInputViewModel.editMessageViewState.observe(this) { state -> + when (state) { + is MessageInputViewModel.EditMessageSuccessState -> { + when (state.messageEdited.ocs?.meta?.statusCode) { + HTTP_BAD_REQUEST -> { + Snackbar.make( + binding.root, + getString(R.string.edit_error_24_hours_old_message), + Snackbar.LENGTH_LONG + ).show() + } + + HTTP_FORBIDDEN -> { + Snackbar.make( + binding.root, + getString(R.string.conversation_is_read_only), + Snackbar.LENGTH_LONG + ).show() + } + + HTTP_NOT_FOUND -> { + Snackbar.make( + binding.root, + "Conversation not found", + Snackbar.LENGTH_LONG + ).show() + } + } + val newString = state.messageEdited.ocs?.data?.parentMessage?.message ?: "(null)" + val id = state.messageEdited.ocs?.data?.parentMessage?.id.toString() + val index = adapter?.getMessagePositionById(id) ?: 0 + val item = adapter?.items?.get(index)?.item + item?.let { + setMessageAsEdited(item as ChatMessage, newString) + } + } + + is MessageInputViewModel.EditMessageErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + + chatViewModel.getVoiceRecordingLocked.observe(this) { showContiniousVoiceRecording -> + if (showContiniousVoiceRecording) { + binding.voiceRecordingLock.visibility = View.GONE + supportFragmentManager.commit { + setReorderingAllowed(true) // apparently used for optimizations + replace(R.id.fragment_container_activity_chat, MessageInputVoiceRecordingFragment()) + } + } else { + supportFragmentManager.commit { + setReorderingAllowed(true) + replace(R.id.fragment_container_activity_chat, getMessageInputFragment()) + } + } + } + + chatViewModel.getVoiceRecordingInProgress.observe(this) { voiceRecordingInProgress -> + VibrationUtils.vibrateShort(context) + if (voiceRecordingInProgress) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + binding.voiceRecordingLock.visibility = if ( + voiceRecordingInProgress && + chatViewModel.getVoiceRecordingLocked.value != true + ) { + View.VISIBLE + } else { + View.GONE + } + } + + chatViewModel.recordTouchObserver.observe(this) { y -> + binding.voiceRecordingLock.y -= y + } + + chatViewModel.unbindRoomResult.observe(this) { uiState -> + when (uiState) { + is ChatViewModel.UnbindRoomUiState.Success -> { + binding.conversationDeleteNotice.visibility = View.GONE + Snackbar.make( + binding.root, + context.getString(R.string.nc_room_retention), + Snackbar.LENGTH_LONG + ).show() + + chatMenu?.removeItem(R.id.conversation_event) + } + + is ChatViewModel.UnbindRoomUiState.Error -> { + Snackbar.make( + binding.root, + context.getString(R.string.nc_common_error_sorry), + Snackbar.LENGTH_LONG + ).show() + } + + else -> {} + } + } + + chatViewModel.outOfOfficeViewState.observe(this) { uiState -> + when (uiState) { + is ChatViewModel.OutOfOfficeUIState.Error -> { + Log.e(TAG, "Error fetching/ no user absence data", uiState.exception) + } + + ChatViewModel.OutOfOfficeUIState.None -> { + } + + is ChatViewModel.OutOfOfficeUIState.Success -> { + binding.outOfOfficeContainer.visibility = View.VISIBLE + + val backgroundColor = colorUtil.getNullSafeColorWithFallbackRes( + conversationUser!!.capabilities!!.themingCapability!!.color, + R.color.colorPrimary + ) + + binding.outOfOfficeContainer.findViewById( + R.id.verticalLine + ).setBackgroundColor(backgroundColor) + val setAlpha = ColorUtils.setAlphaComponent(backgroundColor, OUT_OF_OFFICE_ALPHA) + binding.outOfOfficeContainer.setCardBackgroundColor(setAlpha) + + val startDateTimestamp: Long = uiState.userAbsence.startDate.toLong() + val endDateTimestamp: Long = uiState.userAbsence.endDate.toLong() + + val startDate = Date(startDateTimestamp * ONE_SECOND_IN_MILLIS) + val endDate = Date(endDateTimestamp * ONE_SECOND_IN_MILLIS) + + if (dateUtils.isSameDate(startDate, endDate)) { + binding.outOfOfficeContainer.findViewById(R.id.userAbsenceShortMessage).text = + String.format( + context.resources.getString(R.string.user_absence_for_one_day), + currentConversation?.displayName + ) + binding.outOfOfficeContainer.findViewById(R.id.userAbsencePeriod).visibility = + View.GONE + } else { + val dateFormatter = SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) + val startDateString = dateFormatter.format(startDate) + val endDateString = dateFormatter.format(endDate) + binding.outOfOfficeContainer.findViewById(R.id.userAbsenceShortMessage).text = + String.format( + context.resources.getString(R.string.user_absence), + currentConversation?.displayName + ) + + binding.outOfOfficeContainer.findViewById(R.id.userAbsencePeriod).text = + "$startDateString - $endDateString" + } + + if (uiState.userAbsence.replacementUserDisplayName != null) { + var imageUri = ApiUtils.getUrlForAvatar( + conversationUser?.baseUrl, + uiState.userAbsence + .replacementUserId, + false + ).toUri() + if (DisplayUtils.isDarkModeOn(context)) { + imageUri = ApiUtils.getUrlForAvatarDarkTheme( + conversationUser?.baseUrl, + uiState + .userAbsence + .replacementUserId, + false + ).toUri() + } + binding.outOfOfficeContainer.findViewById(R.id.absenceReplacement).text = + context.resources.getString(R.string.user_absence_replacement) + binding.outOfOfficeContainer.findViewById(R.id.replacement_user_avatar) + .load(imageUri) { + transformations(CircleCropTransformation()) + placeholder(R.drawable.account_circle_96dp) + error(R.drawable.account_circle_96dp) + crossfade(true) + } + binding.outOfOfficeContainer.findViewById(R.id.replacement_user_name).text = + uiState.userAbsence.replacementUserDisplayName + } else { + binding.outOfOfficeContainer.findViewById(R.id.userAbsenceReplacement) + .visibility = View.GONE + } + binding.outOfOfficeContainer.findViewById(R.id.userAbsenceLongMessage).text = + uiState.userAbsence.message + binding.outOfOfficeContainer.findViewById(R.id.avatar_chip).setOnClickListener { + joinOneToOneConversation(uiState.userAbsence.replacementUserId!!) + } + } + } + } + + this.lifecycleScope.launch { + chatViewModel.threadRetrieveState.collect { uiState -> + when (uiState) { + ChatViewModel.ThreadRetrieveUiState.None -> { + } + + is ChatViewModel.ThreadRetrieveUiState.Error -> { + Log.e(TAG, "Error when retrieving thread", uiState.exception) + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + is ChatViewModel.ThreadRetrieveUiState.Success -> { + conversationThreadInfo = uiState.thread + invalidateOptionsMenu() + } + } + } + } + } + + private fun removeUnreadMessagesMarker() { + removeMessageById(UNREAD_MESSAGES_MARKER_ID.toString()) + } + + // do not use adapter.deleteById() as it seems to contain a bug! Use this method instead! + @Suppress("MagicNumber") + private fun removeMessageById(idToDelete: String) { + val indexToDelete = adapter?.getMessagePositionById(idToDelete) + if (indexToDelete != null && indexToDelete != UNREAD_MESSAGES_MARKER_ID) { + // If user sent a message as a first message in todays chat, the temp message will be deleted when + // messages are retrieved from server, but also the date has to be deleted as it will be added again + // when the chat messages are added from server. Otherwise date "Today" would be shown twice. + if (indexToDelete == 0 && (adapter?.items?.get(1))?.item is Date) { + adapter?.items?.removeAt(0) + adapter?.items?.removeAt(0) + adapter?.notifyItemRangeRemoved(indexToDelete, 1) + } else { + adapter?.items?.removeAt(indexToDelete) + adapter?.notifyItemRemoved(indexToDelete) + } + } + } + + fun showConversationDeletionWarning(retentionPeriod: Int) { + binding.conversationDeleteNotice.visibility = View.VISIBLE + binding.conversationDeleteNotice.apply { + isClickable = false + isFocusable = false + bringToFront() + } + val deleteNoticeText = binding.conversationDeleteNotice.findViewById(R.id.deletion_message) + viewThemeUtils.material.themeCardView(binding.conversationDeleteNotice) + + deleteNoticeText.text = resources.getQuantityString( + R.plurals.nc_conversation_auto_delete_info, + retentionPeriod, + retentionPeriod + ) + viewThemeUtils.material.colorMaterialButtonPrimaryTonal( + binding.conversationDeleteNotice + .findViewById(R.id.keep_button) + ) + + if (ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!)) { + binding.conversationDeleteNotice.findViewById(R.id.delete_now_button).visibility = + View.VISIBLE + binding.conversationDeleteNotice.findViewById(R.id.keep_button).visibility = View.VISIBLE + } else { + binding.conversationDeleteNotice.findViewById(R.id.delete_now_button).visibility = + View.GONE + binding.conversationDeleteNotice.findViewById(R.id.keep_button).visibility = View.GONE + } + binding.conversationDeleteNotice.findViewById(R.id.delete_now_button).setOnClickListener { + deleteConversationDialog(it.context) + } + + binding.conversationDeleteNotice.findViewById(R.id.keep_button).setOnClickListener { + chatViewModel.unbindRoom(credentials!!, conversationUser?.baseUrl!!, currentConversation?.token!!) + } + } + + fun deleteConversationDialog(context: Context) { + val dialogBuilder = MaterialAlertDialogBuilder(context) + .setIcon( + viewThemeUtils.dialog + .colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp) + ) + .setTitle(R.string.nc_delete_call) + .setMessage(R.string.nc_delete_conversation_more) + .setPositiveButton(R.string.nc_delete) { _, _ -> + currentConversation?.let { conversation -> + deleteConversation(conversation) + } + } + .setNegativeButton(R.string.nc_cancel) { _, _ -> + } + + viewThemeUtils.dialog + .colorMaterialAlertDialogBackground(context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onResume() { + super.onResume() + + logConversationInfos("onResume") + + pullChatMessagesPending = false + + webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener) + webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener) + + cancelNotificationsForCurrentConversation() + + chatViewModel.getRoom(roomToken) + + actionBar?.show() + + setupSwipeToReply() + + binding.unreadMessagesPopup.setOnClickListener { + binding.messagesListView.smoothScrollToPosition(0) + binding.unreadMessagesPopup.visibility = View.GONE + } + + binding.scrollDownButton.setOnClickListener { + binding.messagesListView.scrollToPosition(0) + it.visibility = View.GONE + } + + binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it.scrollDownButton) } + + binding.let { viewThemeUtils.material.themeFAB(it.voiceRecordingLock) } + + binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it.unreadMessagesPopup) } + + binding.messagesListView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + + if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { + if (isScrolledToBottom()) { + binding.unreadMessagesPopup.visibility = View.GONE + binding.scrollDownButton.visibility = View.GONE + } else { + if (binding.unreadMessagesPopup.isShown) { + binding.scrollDownButton.visibility = View.GONE + } else { + binding.scrollDownButton.visibility = View.VISIBLE + } + } + } + } + }) + + loadAvatarForStatusBar() + setActionBarTitle() + viewThemeUtils.material.colorToolbarOverflowIcon(binding.chatToolbar) + } + + // private fun getLastAdapterId(): Int { + // var lastId = 0 + // if (adapter?.items?.size != 0) { + // val item = adapter?.items?.get(0)?.item + // if (item != null) { + // lastId = (item as ChatMessage).jsonMessageId + // } else { + // lastId = 0 + // } + // } + // return lastId + // } + + private fun setupActionBar() { + setSupportActionBar(binding.chatToolbar) + binding.chatToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(R.color.transparent, null).toDrawable()) + setActionBarTitle() + viewThemeUtils.material.themeToolbar(binding.chatToolbar) + } + + private fun initAdapter() { + val senderId = if (!conversationUser!!.userId.equals("?")) { + "users/" + conversationUser!!.userId + } else { + currentConversation?.actorType + "/" + currentConversation?.actorId + } + + Log.d(TAG, "Initialize TalkMessagesListAdapter with senderId: $senderId") + + adapter = TalkMessagesListAdapter( + senderId, + initMessageHolders(), + ImageLoader { imageView, url, placeholder -> + imageView.loadAvatarOrImagePreview(url!!, conversationUser!!, placeholder as Drawable?) + }, + this + ) + + adapter?.setLoadMoreListener(this) + adapter?.setDateHeadersFormatter { format(it) } + adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) } + + adapter?.registerViewClickListener( + R.id.playPauseBtn + ) { _, message -> + val filename = message.selectedIndividualHashMap!!["name"] + val file = File(context.cacheDir, filename!!) + if (file.exists()) { + if (message.isPlayingVoiceMessage) { + chatViewModel.pauseMediaPlayer(true) + message.isPlayingVoiceMessage = false + adapter?.update(message) + } else { + val retrieved = appPreferences.getWaveFormFromFile(filename) + if (retrieved.isEmpty()) { + setUpWaveform(message) + } else { + startPlayback(file, message) + } + } + } else { + Log.d(TAG, "Downloaded to cache") + downloadFileToCache(message, true) { + setUpWaveform(message) + } + } + } + + adapter?.registerViewClickListener(R.id.playbackSpeedControlBtn) { button, message -> + val nextSpeed = (button as PlaybackSpeedControl).getSpeed().next() + chatViewModel.setPlayBack(nextSpeed) + appPreferences.savePreferredPlayback(conversationUser!!.userId, nextSpeed) + } + } + + private fun setUpWaveform(message: ChatMessage, thenPlay: Boolean = true, backgroundPlayAllowed: Boolean = false) { + val filename = message.selectedIndividualHashMap!!["name"] + val file = File(context.cacheDir, filename!!) + if (file.exists() && message.voiceMessageFloatArray == null) { + message.isDownloadingVoiceMessage = true + adapter?.update(message) + CoroutineScope(Dispatchers.Default).launch { + val r = AudioUtils.audioFileToFloatArray(file) + appPreferences.saveWaveFormForFile(filename, r.toTypedArray()) + message.voiceMessageFloatArray = r + withContext(Dispatchers.Main) { + startPlayback(file, message) + } + } + } else { + startPlayback(file, message) + } + } + + private fun startPlayback(file: File, message: ChatMessage) { + chatViewModel.clearMediaPlayerQueue() + chatViewModel.queueInMediaPlayer(file.canonicalPath, message) + chatViewModel.startCyclingMediaPlayer() + message.isPlayingVoiceMessage = true + adapter?.update(message) + + var pos = adapter?.getMessagePositionById(message.id)!! - 1 + do { + if (pos < 0) break + val nextItem = (adapter?.items?.get(pos)?.item) ?: break + val nextMessage = if (nextItem is ChatMessage) nextItem else break + if (!nextMessage.isVoiceMessage) break + + downloadFileToCache(nextMessage, false) { + val newFilename = nextMessage.selectedIndividualHashMap!!["name"] + val newFile = File(context.cacheDir, newFilename!!) + chatViewModel.queueInMediaPlayer(newFile.canonicalPath, nextMessage) + } + pos-- + } while (true && pos >= 0) + } + + @Suppress("LongMethod") + private fun initMessageHolders(): MessageHolders { + val messageHolders = MessageHolders() + val profileBottomSheet = ProfileBottomSheet(ncApi, conversationUser!!, viewThemeUtils) + + val payload = MessagePayload( + roomToken, + ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!), + profileBottomSheet + ) + + messageHolders.setIncomingTextConfig( + IncomingTextMessageViewHolder::class.java, + R.layout.item_custom_incoming_text_message, + payload + ) + messageHolders.setOutcomingTextConfig( + OutcomingTextMessageViewHolder::class.java, + R.layout.item_custom_outcoming_text_message + ) + + messageHolders.setIncomingImageConfig( + IncomingPreviewMessageViewHolder::class.java, + R.layout.item_custom_incoming_preview_message, + payload + ) + + messageHolders.setOutcomingImageConfig( + OutcomingPreviewMessageViewHolder::class.java, + R.layout.item_custom_outcoming_preview_message + ) + + messageHolders.registerContentType( + CONTENT_TYPE_SYSTEM_MESSAGE, + SystemMessageViewHolder::class.java, + R.layout.item_system_message, + SystemMessageViewHolder::class.java, + R.layout.item_system_message, + this + ) + messageHolders.registerContentType( + CONTENT_TYPE_UNREAD_NOTICE_MESSAGE, + UnreadNoticeMessageViewHolder::class.java, + R.layout.item_date_header, + UnreadNoticeMessageViewHolder::class.java, + R.layout.item_date_header, + this + ) + + messageHolders.registerContentType( + CONTENT_TYPE_LOCATION, + IncomingLocationMessageViewHolder::class.java, + payload, + R.layout.item_custom_incoming_location_message, + OutcomingLocationMessageViewHolder::class.java, + null, + R.layout.item_custom_outcoming_location_message, + this + ) + + messageHolders.registerContentType( + CONTENT_TYPE_VOICE_MESSAGE, + IncomingVoiceMessageViewHolder::class.java, + payload, + R.layout.item_custom_incoming_voice_message, + OutcomingVoiceMessageViewHolder::class.java, + null, + R.layout.item_custom_outcoming_voice_message, + this + ) + + messageHolders.registerContentType( + CONTENT_TYPE_POLL, + IncomingPollMessageViewHolder::class.java, + payload, + R.layout.item_custom_incoming_poll_message, + OutcomingPollMessageViewHolder::class.java, + payload, + R.layout.item_custom_outcoming_poll_message, + this + ) + + messageHolders.registerContentType( + CONTENT_TYPE_LINK_PREVIEW, + IncomingLinkPreviewMessageViewHolder::class.java, + payload, + R.layout.item_custom_incoming_link_preview_message, + OutcomingLinkPreviewMessageViewHolder::class.java, + payload, + R.layout.item_custom_outcoming_link_preview_message, + this + ) + + messageHolders.registerContentType( + CONTENT_TYPE_DECK_CARD, + IncomingDeckCardViewHolder::class.java, + payload, + R.layout.item_custom_incoming_deck_card_message, + OutcomingDeckCardViewHolder::class.java, + payload, + R.layout.item_custom_outcoming_deck_card_message, + this + ) + + return messageHolders + } + + @Suppress("MagicNumber", "LongMethod") + private fun updateTypingIndicator() { + fun ellipsize(text: String): String = DisplayUtils.ellipsize(text, TYPING_INDICATOR_MAX_NAME_LENGTH) + + val participantNames = ArrayList() + + for (typingParticipant in typingParticipants.values) { + participantNames.add(typingParticipant.name) + } + + val typingString: SpannableStringBuilder + when (typingParticipants.size) { + 0 -> typingString = SpannableStringBuilder().append(binding.typingIndicator.text) + + // person1 is typing + 1 -> typingString = SpannableStringBuilder() + .bold { append(ellipsize(participantNames[0])) } + .append(WHITESPACE + context.resources?.getString(R.string.typing_is_typing)) + + // person1 and person2 are typing + 2 -> typingString = SpannableStringBuilder() + .bold { append(ellipsize(participantNames[0])) } + .append(WHITESPACE + context.resources?.getString(R.string.nc_common_and) + WHITESPACE) + .bold { append(ellipsize(participantNames[1])) } + .append(WHITESPACE + context.resources?.getString(R.string.typing_are_typing)) + + // person1, person2 and person3 are typing + 3 -> typingString = SpannableStringBuilder() + .bold { append(ellipsize(participantNames[0])) } + .append(COMMA) + .bold { append(ellipsize(participantNames[1])) } + .append(WHITESPACE + context.resources?.getString(R.string.nc_common_and) + WHITESPACE) + .bold { append(ellipsize(participantNames[2])) } + .append(WHITESPACE + context.resources?.getString(R.string.typing_are_typing)) + + // person1, person2, person3 and 1 other is typing + 4 -> typingString = SpannableStringBuilder() + .bold { append(participantNames[0]) } + .append(COMMA) + .bold { append(participantNames[1]) } + .append(COMMA) + .bold { append(participantNames[2]) } + .append(WHITESPACE + context.resources?.getString(R.string.typing_1_other)) + + // person1, person2, person3 and x others are typing + else -> { + val moreTypersAmount = typingParticipants.size - 3 + val othersTyping = context.resources?.getString(R.string.typing_x_others)?.let { + String.format(it, moreTypersAmount) + } + typingString = SpannableStringBuilder() + .bold { append(participantNames[0]) } + .append(COMMA) + .bold { append(participantNames[1]) } + .append(COMMA) + .bold { append(participantNames[2]) } + .append(othersTyping) + } + } + + runOnUiThread { + binding.typingIndicator.text = typingString + + val typingIndicatorPositionY = if (participantNames.size > 0) { + TYPING_INDICATOR_POSITION_VISIBLE + } else { + TYPING_INDICATOR_POSITION_HIDDEN + } + + binding.typingIndicatorWrapper.animate() + .translationY(DisplayUtils.convertDpToPixel(typingIndicatorPositionY, context)) + .setInterpolator(AccelerateDecelerateInterpolator()) + .duration = TYPING_INDICATOR_ANIMATION_DURATION + } + } + + private fun isTypingStatusEnabled(): Boolean = + webSocketInstance != null && + !CapabilitiesUtil.isTypingStatusPrivate(conversationUser!!) + + private fun setupSwipeToReply() { + if (this::participantPermissions.isInitialized && + participantPermissions.hasChatPermission() && + !isReadOnlyConversation() + ) { + val messageSwipeCallback = MessageSwipeCallback( + this, + object : MessageSwipeActions { + override fun showReplyUI(position: Int) { + val chatMessage = adapter?.items?.getOrNull(position)?.item as ChatMessage? + if (chatMessage != null) { + messageInputViewModel.reply(chatMessage) + } + } + } + ) + + val itemTouchHelper = ItemTouchHelper(messageSwipeCallback) + itemTouchHelper.attachToRecyclerView(binding.messagesListView) + } + } + + private fun loadAvatarForStatusBar() { + if (currentConversation == null) { + return + } + + if (isOneToOneConversation()) { + var url = ApiUtils.getUrlForAvatar( + conversationUser!!.baseUrl!!, + currentConversation!!.name, + true + ) + + if (DisplayUtils.isDarkModeOn(supportActionBar?.themedContext!!)) { + url = "$url/dark" + } + + val target = object : Target { + + private fun setIcon(drawable: Drawable?) { + supportActionBar?.let { + val avatarSize = (it.height / TOOLBAR_AVATAR_RATIO).roundToInt() + val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, context) + if (drawable != null && avatarSize > 0) { + val bitmap = drawable.toBitmap(avatarSize, avatarSize) + val status = StatusDrawable( + currentConversation!!.status, + null, + size, + 0, + binding.chatToolbar.context + ) + viewThemeUtils.talk.themeStatusDrawable(context, status) + binding.chatToolbar.findViewById(R.id.chat_toolbar_avatar) + .setImageDrawable(bitmap.toDrawable(resources)) + binding.chatToolbar.findViewById(R.id.chat_toolbar_status) + .setImageDrawable(status) + binding.chatToolbar.findViewById(R.id.chat_toolbar_status).contentDescription = + currentConversation?.status + binding.chatToolbar.findViewById(R.id.chat_toolbar_avatar_container) + .visibility = View.VISIBLE + } else { + Log.d(TAG, "loadAvatarForStatusBar avatarSize <= 0") + } + } + } + + override fun onStart(placeholder: Drawable?) { + this.setIcon(placeholder) + } + + override fun onSuccess(result: Drawable) { + this.setIcon(result) + } + } + + val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token) + if (credentials != null) { + context.imageLoader.enqueue( + ImageRequest.Builder(context) + .data(url) + .addHeader("Authorization", credentials) + .transformations(CircleCropTransformation()) + .crossfade(true) + .target(target) + .memoryCachePolicy(CachePolicy.DISABLED) + .diskCachePolicy(CachePolicy.DISABLED) + .build() + ) + } + } else { + binding.chatToolbar.findViewById(R.id.chat_toolbar_avatar_container).visibility = View.GONE + } + } + + fun isOneToOneConversation() = + currentConversation != null && + currentConversation?.type != null && + currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + + private fun isGroupConversation() = + currentConversation != null && + currentConversation?.type != null && + currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL + + private fun isPublicConversation() = + currentConversation != null && + currentConversation?.type != null && + currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL + + private fun updateRoomTimerHandler(delay: Long = -1) { + val delayForRecursiveCall = if (shouldShowLobby()) { + GET_ROOM_INFO_DELAY_LOBBY + } else { + GET_ROOM_INFO_DELAY_NORMAL + } + + if (getRoomInfoTimerHandler == null) { + getRoomInfoTimerHandler = Handler() + } + getRoomInfoTimerHandler?.postDelayed( + { + chatViewModel.getRoom(roomToken) + }, + if (delay > 0) delay else delayForRecursiveCall + ) + } + + private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) { + if (conversationUser != null) { + runOnUiThread { + val toastInfo = if (currentConversation?.objectType == ConversationEnums.ObjectType.ROOM) { + context.resources.getString(R.string.switch_to_main_room) + } else { + context.resources.getString(R.string.switch_to_breakout_room) + } + // do not replace with snackbar, as it would disappear with the activity switch + Toast.makeText( + context, + toastInfo, + Toast.LENGTH_LONG + ).show() + } + + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, token) + + if (startCallAfterRoomSwitch) { + bundle.putBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, true) + bundle.putBoolean(KEY_CALL_VOICE_ONLY, isVoiceOnlyCall) + } + + leaveRoom { + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(chatIntent) + } + } + } + + private fun showCallButtonMenu(isVoiceOnlyCall: Boolean) { + val anchor: View? = if (isVoiceOnlyCall) { + findViewById(R.id.conversation_voice_call) + } else { + findViewById(R.id.conversation_video_call) + } + + if (anchor != null) { + val popupMenu = PopupMenu( + ContextThemeWrapper(this, R.style.CallButtonMenu), + anchor, + Gravity.END + ) + popupMenu.inflate(R.menu.chat_call_menu) + + popupMenu.setOnMenuItemClickListener { item: MenuItem -> + when (item.itemId) { + R.id.call_without_notification -> startACall(isVoiceOnlyCall, true) + } + true + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + popupMenu.setForceShowIcon(true) + } + popupMenu.show() + } + } + + override fun updateMediaPlayerProgressBySlider(message: ChatMessage, progress: Int) { + chatViewModel.seekToMediaPlayer(progress) + } + + override fun registerMessageToObservePlaybackSpeedPreferences( + userId: String, + listener: (speed: PlaybackSpeed) -> Unit + ) { + CoroutineScope(Dispatchers.Default).launch { + chatViewModel.voiceMessagePlayBackUIFlow.onEach { speed -> + withContext(Dispatchers.Main) { + listener(speed) + } + }.collect() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun collapseSystemMessages() { + adapter?.items?.forEach { + if (it.item is ChatMessage) { + val chatMessage = it.item as ChatMessage + if (isChildOfExpandableSystemMessage(chatMessage)) { + chatMessage.hiddenByCollapse = true + } + chatMessage.isExpanded = false + } + } + + adapter?.notifyDataSetChanged() + } + + private fun isChildOfExpandableSystemMessage(chatMessage: ChatMessage): Boolean = + isSystemMessage(chatMessage) && + !chatMessage.expandableParent && + chatMessage.lastItemOfExpandableGroup != 0 + + @SuppressLint("NotifyDataSetChanged") + override fun expandSystemMessage(chatMessageToExpand: ChatMessage) { + adapter?.items?.forEach { + if (it.item is ChatMessage) { + val belongsToGroupToExpand = + (it.item as ChatMessage).lastItemOfExpandableGroup == chatMessageToExpand.lastItemOfExpandableGroup + + if (belongsToGroupToExpand) { + (it.item as ChatMessage).hiddenByCollapse = false + } + } + } + + chatMessageToExpand.isExpanded = true + + adapter?.notifyDataSetChanged() + } + + @SuppressLint("LongLogTag") + private fun downloadFileToCache( + message: ChatMessage, + openWhenDownloaded: Boolean, + funToCallWhenDownloadSuccessful: (() -> Unit) + ) { + message.isDownloadingVoiceMessage = true + message.openWhenDownloaded = openWhenDownloaded + adapter?.update(message) + + val baseUrl = message.activeUser!!.baseUrl + val userId = message.activeUser!!.userId + val attachmentFolder = CapabilitiesUtil.getAttachmentFolder( + message.activeUser!!.capabilities!! + .spreedCapability!! + ) + val fileName = message.selectedIndividualHashMap!!["name"] + var size = message.selectedIndividualHashMap!!["size"] + if (size == null) { + size = "-1" + } + val fileSize = size.toLong() + val fileId = message.selectedIndividualHashMap!!["id"] + val path = message.selectedIndividualHashMap!!["path"] + + // check if download worker is already running + val workers = WorkManager.getInstance( + context + ).getWorkInfosByTag(fileId!!) + try { + for (workInfo in workers.get()) { + if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { + Log.d(TAG, "Download worker for $fileId is already running or scheduled") + return + } + } + } catch (e: ExecutionException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } catch (e: InterruptedException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } + + val data: Data = Data.Builder() + .putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl) + .putString(DownloadFileToCacheWorker.KEY_USER_ID, userId) + .putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder) + .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName) + .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path) + .putLong(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize) + .build() + + val downloadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java) + .setInputData(data) + .addTag(fileId) + .build() + + WorkManager.getInstance().enqueue(downloadWorker) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id) + .observeForever { workInfo: WorkInfo? -> + if (workInfo?.state == WorkInfo.State.SUCCEEDED) { + funToCallWhenDownloadSuccessful() + } + } + } + + fun isRecordAudioPermissionGranted(): Boolean = + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.RECORD_AUDIO + ) == PERMISSION_GRANTED + + fun requestRecordAudioPermissions() { + requestPermissions( + arrayOf( + Manifest.permission.RECORD_AUDIO + ), + REQUEST_RECORD_AUDIO_PERMISSION + ) + } + + private fun requestCameraPermissions() { + requestPermissions( + arrayOf( + Manifest.permission.CAMERA + ), + REQUEST_CAMERA_PERMISSION + ) + } + + private fun requestReadContacts() { + requestPermissions( + arrayOf( + Manifest.permission.READ_CONTACTS + ), + REQUEST_READ_CONTACT_PERMISSION + ) + } + + private fun requestReadFilesPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissions( + arrayOf( + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.READ_MEDIA_AUDIO + ), + REQUEST_SHARE_FILE_PERMISSION + ) + } else { + requestPermissions( + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE + ), + REQUEST_SHARE_FILE_PERMISSION + ) + } + } + + private fun checkShowCallButtons() { + if (isReadOnlyConversation() || + shouldShowLobby() || + ConversationUtils.isNoteToSelfConversation(currentConversation) + ) { + disableCallButtons() + } else { + enableCallButtons() + } + } + + private fun checkShowMessageInputView() { + if (isReadOnlyConversation() || + shouldShowLobby() || + !participantPermissions.hasChatPermission() + ) { + binding.fragmentContainerActivityChat.visibility = View.GONE + } else { + binding.fragmentContainerActivityChat.visibility = View.VISIBLE + } + } + + private fun shouldShowLobby(): Boolean { + if (currentConversation != null) { + return hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) && + currentConversation?.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY && + !ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) && + !participantPermissions.canIgnoreLobby() + } + return false + } + + private fun disableCallButtons() { + if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) { + if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) { + conversationVoiceCallMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT + conversationVideoMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT + conversationVoiceCallMenuItem?.isEnabled = false + conversationVideoMenuItem?.isEnabled = false + } else { + Log.e(TAG, "call buttons were null when trying to disable them") + } + } + } + + private fun enableCallButtons() { + if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) { + if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) { + conversationVoiceCallMenuItem?.icon?.alpha = FULLY_OPAQUE_INT + conversationVideoMenuItem?.icon?.alpha = FULLY_OPAQUE_INT + conversationVoiceCallMenuItem?.isEnabled = true + conversationVideoMenuItem?.isEnabled = true + } else { + Log.e(TAG, "call buttons were null when trying to enable them") + } + } + } + + private fun isEventConversation() { + if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) { + if (eventConversationMenuItem != null) { + eventConversationMenuItem?.icon?.alpha = FULLY_OPAQUE_INT + eventConversationMenuItem?.isEnabled = true + } + } else { + eventConversationMenuItem?.isEnabled = false + } + } + + private fun isReadOnlyConversation(): Boolean = + currentConversation?.conversationReadOnlyState != null && + currentConversation?.conversationReadOnlyState == + ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY + + private fun checkLobbyState() { + if (currentConversation != null && + ConversationUtils.isLobbyViewApplicable(currentConversation!!, spreedCapabilities) && + shouldShowLobby() + ) { + showLobbyView() + } else { + binding.lobby.lobbyView.visibility = View.GONE + binding.messagesListView.visibility = View.VISIBLE + checkShowMessageInputView() + } + } + + private fun showLobbyView() { + binding.lobby.lobbyView.visibility = View.VISIBLE + binding.messagesListView.visibility = View.GONE + binding.fragmentContainerActivityChat.visibility = View.GONE + binding.progressBar.visibility = View.GONE + + val sb = StringBuilder() + sb.append(resources!!.getText(R.string.nc_lobby_waiting)) + .append("\n\n") + + if (currentConversation?.lobbyTimer != null && + currentConversation?.lobbyTimer != + 0L + ) { + val timestampMS = (currentConversation?.lobbyTimer ?: 0) * DateConstants.SECOND_DIVIDER + val stringWithStartDate = String.format( + resources!!.getString(R.string.nc_lobby_start_date), + dateUtils.getLocalDateTimeStringFromTimestamp(timestampMS) + ) + val relativeTime = dateUtils.relativeStartTimeForLobby(timestampMS, resources!!) + + sb.append("$stringWithStartDate - $relativeTime") + .append("\n\n") + } + + sb.append(currentConversation!!.description) + binding.lobby.lobbyTextView.text = sb.toString() + } + + private fun onRemoteFileBrowsingResult(intent: Intent?) { + val pathList = intent?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS) + if (pathList?.size!! >= 1) { + pathList + .chunked(CHUNK_SIZE) + .forEach { paths -> + val data = Data.Builder() + .putLong(KEY_INTERNAL_USER_ID, conversationUser!!.id!!) + .putString(KEY_ROOM_TOKEN, roomToken) + .putStringArray(KEY_FILE_PATHS, paths.toTypedArray()) + .build() + val worker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java) + .setInputData(data) + .build() + WorkManager.getInstance().enqueue(worker) + } + } + } + + private fun onChooseFileResult(intent: Intent?) { + try { + checkNotNull(intent) + val fileUris = mutableListOf() + intent.clipData?.let { + for (index in 0 until it.itemCount) { + fileUris.add(it.getItemAt(index).uri) + } + } ?: run { + checkNotNull(intent.data) + intent.data.let { + fileUris.add(intent.data!!) + } + } + onChooseFileResult(fileUris) + } catch (e: IllegalStateException) { + context.resources?.getString(R.string.nc_upload_failed)?.let { + Snackbar.make( + binding.root, + it, + Snackbar.LENGTH_LONG + ).show() + } + Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e) + } + } + + private fun onChooseFileResult(filesToUpload: List) { + try { + require(filesToUpload.isNotEmpty()) + + val filenamesWithLineBreaks = StringBuilder("\n") + + for (file in filesToUpload) { + val filename = FileUtils.getFileName(file, context) + filenamesWithLineBreaks.append(filename).append("\n") + } + + val newFragment = FileAttachmentPreviewFragment.newInstance( + filenamesWithLineBreaks.toString(), + filesToUpload.map { it.toString() }.toMutableList() + ) + newFragment.setListener { files, caption -> + uploadFiles(files, caption) + } + newFragment.show(supportFragmentManager, FileAttachmentPreviewFragment.TAG) + } catch (e: IllegalStateException) { + context.resources?.getString(R.string.nc_upload_failed)?.let { + Snackbar.make( + binding.root, + it, + Snackbar.LENGTH_LONG + ).show() + } + Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e) + } catch (e: IllegalArgumentException) { + context.resources?.getString(R.string.nc_upload_failed)?.let { + Snackbar.make( + binding.root, + it, + Snackbar.LENGTH_LONG + ).show() + } + Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e) + } + } + + private fun onSelectContactResult(intent: Intent?) { + val contactUri = intent?.data ?: return + val cursor: Cursor? = contentResolver!!.query(contactUri, null, null, null, null) + + if (cursor != null && cursor.moveToFirst()) { + val id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID)) + val fileName = ContactUtils.getDisplayNameFromDeviceContact(context, id) + ".vcf" + val file = File(context.cacheDir, fileName) + writeContactToVcfFile(cursor, file) + + val shareUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID, + File(file.absolutePath) + ) + uploadFile( + fileUri = shareUri.toString(), + isVoiceMessage = false, + caption = "", + roomToken = roomToken, + replyToMessageId = getReplyToMessageId(), + displayName = currentConversation?.displayName ?: "" + ) + } + cursor?.close() + } + + fun getReplyToMessageId(): Int { + var replyMessageId = messageInputViewModel.getReplyChatMessage.value?.id?.toInt() + if (replyMessageId == null || replyMessageId == 0) { + replyMessageId = conversationThreadInfo?.thread?.id ?: 0 + } + return replyMessageId + } + + @Throws(IllegalStateException::class) + private fun onPickCameraResult(intent: Intent?) { + try { + filesToUpload.clear() + + if (intent != null && intent.data != null) { + run { + intent.data.let { + filesToUpload.add(intent.data.toString()) + } + } + require(filesToUpload.isNotEmpty()) + } else if (videoURI != null) { + filesToUpload.add(videoURI.toString()) + videoURI = null + } else { + error("Failed to get data from intent and uri") + } + + if (permissionUtil.isFilesPermissionGranted()) { + val filenamesWithLineBreaks = StringBuilder("\n") + + for (file in filesToUpload) { + val filename = FileUtils.getFileName(file.toUri(), context) + filenamesWithLineBreaks.append(filename).append("\n") + } + + val newFragment = FileAttachmentPreviewFragment.newInstance( + filenamesWithLineBreaks.toString(), + filesToUpload + ) + newFragment.setListener { files, caption -> uploadFiles(files, caption) } + newFragment.show(supportFragmentManager, FileAttachmentPreviewFragment.TAG) + } else { + UploadAndShareFilesWorker.requestStoragePermission(this) + } + } catch (e: IllegalStateException) { + Snackbar.make( + binding.root, + R.string.nc_upload_failed, + Snackbar.LENGTH_LONG + ) + .show() + Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e) + } catch (e: IllegalArgumentException) { + context.resources?.getString(R.string.nc_upload_failed)?.let { + Snackbar.make( + binding.root, + it, + Snackbar.LENGTH_LONG + ) + .show() + } + Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e) + } + } + + private fun executeIfResultOk(result: ActivityResult, onResult: (intent: Intent?) -> Unit) { + if (result.resultCode == RESULT_OK) { + onResult(result.data) + } else { + Log.e(TAG, "resultCode for received intent was != ok") + } + } + + private fun scrollToMessageWithId(messageId: String) { + val position = adapter?.items?.indexOfFirst { + it.item is ChatMessage && (it.item as ChatMessage).id == messageId + } + if (position != null && position >= 0) { + binding.messagesListView.scrollToPosition(position) + } else { + Log.d(TAG, "message $messageId that should be scrolled to was not found (scrollToMessageWithId)") + } + } + + private fun scrollToAndCenterMessageWithId(messageId: String) { + adapter?.let { + val position = it.getMessagePositionByIdInReverse(messageId) + if (position != -1) { + layoutManager?.scrollToPositionWithOffset( + position, + binding.messagesListView.height / 2 + ) + } else { + Log.d( + TAG, + "message $messageId that should be scrolled " + + "to was not found (scrollToAndCenterMessageWithId)" + ) + } + } + } + + private fun writeContactToVcfFile(cursor: Cursor, file: File) { + val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY)) + val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey) + + val fd: AssetFileDescriptor = contentResolver!!.openAssetFileDescriptor(uri, "r")!! + fd.use { + val fis = fd.createInputStream() + + file.createNewFile() + fis.use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "upload starting after permissions were granted") + if (filesToUpload.isNotEmpty()) { + uploadFiles(filesToUpload) + } + } else { + Snackbar + .make(binding.root, context.getString(R.string.read_storage_no_permission), Snackbar.LENGTH_LONG) + .show() + } + } else if (requestCode == REQUEST_SHARE_FILE_PERMISSION) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showLocalFilePicker() + } else { + Snackbar.make( + binding.root, + context.getString(R.string.nc_file_storage_permission), + Snackbar.LENGTH_LONG + ).show() + } + } else if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // do nothing. user will tap on the microphone again if he wants to record audio.. + } else { + Snackbar.make( + binding.root, + context.getString(R.string.nc_voice_message_missing_audio_permission), + Snackbar.LENGTH_LONG + ).show() + } + } else if (requestCode == REQUEST_READ_CONTACT_PERMISSION) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI) + startSelectContactForResult.launch(intent) + } else { + Snackbar.make( + binding.root, + context.getString(R.string.nc_share_contact_permission), + Snackbar.LENGTH_LONG + ).show() + } + } else if (requestCode == REQUEST_CAMERA_PERMISSION) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Snackbar + .make(binding.root, context.getString(R.string.camera_permission_granted), Snackbar.LENGTH_LONG) + .show() + } else { + Snackbar + .make(binding.root, context.getString(R.string.take_photo_permission), Snackbar.LENGTH_LONG) + .show() + } + } + } + + private fun uploadFiles(files: MutableList, caption: String = "") { + for (i in 0 until files.size) { + if (i == files.size - 1) { + uploadFile( + fileUri = files[i], + isVoiceMessage = false, + caption = caption, + roomToken = roomToken, + replyToMessageId = getReplyToMessageId(), + displayName = currentConversation?.displayName!! + ) + } else { + uploadFile( + fileUri = files[i], + isVoiceMessage = false, + caption = "", + roomToken = roomToken, + replyToMessageId = getReplyToMessageId(), + displayName = currentConversation?.displayName!! + ) + } + } + } + + fun showGalleryPicker() { + pickMultipleMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo)) + } + + private fun showLocalFilePicker() { + val action = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + type = "*/*" + addCategory(Intent.CATEGORY_OPENABLE) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + startChooseFileIntentForResult.launch( + Intent.createChooser( + action, + context.resources?.getString( + R.string.nc_upload_choose_local_files + ) + ) + ) + } + + fun sendSelectLocalFileIntent() { + if (!permissionUtil.isFilesPermissionGranted()) { + requestReadFilesPermissions() + } else { + showLocalFilePicker() + } + } + + fun sendChooseContactIntent() { + requestReadContacts() + } + + fun showBrowserScreen() { + val sharingFileBrowserIntent = Intent(this, RemoteFileBrowserActivity::class.java) + startRemoteFileBrowsingForResult.launch(sharingFileBrowserIntent) + } + + fun showShareLocationScreen() { + Log.d(TAG, "showShareLocationScreen") + + val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager + val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + + if (!isGpsEnabled) { + showLocationServicesDisabledDialog() + } else if (!permissionUtil.isLocationPermissionGranted()) { + showLocationPermissionDeniedDialog() + } + + if (permissionUtil.isLocationPermissionGranted() && isGpsEnabled) { + val intent = Intent(this, LocationPickerActivity::class.java) + intent.putExtra(KEY_ROOM_TOKEN, roomToken) + intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion) + startActivity(intent) + } + } + + private fun showLocationServicesDisabledDialog() { + val title = resources.getString(R.string.location_services_disabled) + val explanation = resources.getString(R.string.location_services_disabled_msg) + val positive = resources.getString(R.string.nc_permissions_settings) + val cancel = resources.getString(R.string.nc_cancel) + val dialogBuilder = MaterialAlertDialogBuilder(this) + .setTitle(title) + .setMessage(explanation) + .setPositiveButton(positive) { _, _ -> + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + startActivity(intent) + } + .setNegativeButton(cancel, null) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + + private fun showLocationPermissionDeniedDialog() { + val title = resources.getString(R.string.location_permission_denied) + val explanation = resources.getString(R.string.location_permission_denied_msg) + val positive = resources.getString(R.string.nc_permissions_settings) + val cancel = resources.getString(R.string.nc_cancel) + val dialogBuilder = MaterialAlertDialogBuilder(this) + .setTitle(title) + .setMessage(explanation) + .setPositiveButton(positive) { _, _ -> + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", packageName, null) + } + startActivity(intent) + } + .setNegativeButton(cancel, null) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + + private fun showConversationInfoScreen() { + val bundle = Bundle() + + bundle.putString(KEY_ROOM_TOKEN, roomToken) + bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation()) + + val intent = Intent(this, ConversationInfoActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } + + private fun validSessionId(): Boolean = + currentConversation != null && + sessionIdAfterRoomJoined?.isNotEmpty() == true && + sessionIdAfterRoomJoined != "0" + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun cancelNotificationsForCurrentConversation() { + if (conversationUser != null) { + if (!TextUtils.isEmpty(roomToken)) { + try { + NotificationUtils.cancelExistingNotificationsForRoom( + applicationContext, + conversationUser!!, + roomToken + ) + } catch (e: RuntimeException) { + Log.w(TAG, "Cancel notifications for current conversation results with an error.", e) + } + } + } + } + + override fun onPause() { + super.onPause() + + logConversationInfos("onPause") + + eventBus.unregister(this) + + webSocketInstance?.getSignalingMessageReceiver()?.removeListener(localParticipantMessageListener) + webSocketInstance?.getSignalingMessageReceiver()?.removeListener(conversationMessageListener) + + findViewById(R.id.toolbar)?.setOnClickListener(null) + + checkingLobbyStatus = false + + if (getRoomInfoTimerHandler != null) { + getRoomInfoTimerHandler?.removeCallbacksAndMessages(null) + } + + if (conversationUser != null && isActivityNotChangingConfigurations() && isNotInCall()) { + ApplicationWideCurrentRoomHolder.getInstance().clear() + if (validSessionId()) { + leaveRoom(null) + } else { + Log.d(TAG, "not leaving room (validSessionId is false)") + } + } else { + Log.d(TAG, "not leaving room...") + } + + if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { + mentionAutocomplete?.dismissPopup() + } + adapter = null + } + + private fun isActivityNotChangingConfigurations(): Boolean = !isChangingConfigurations + + private fun isNotInCall(): Boolean = + !ApplicationWideCurrentRoomHolder.getInstance().isInCall && + !ApplicationWideCurrentRoomHolder.getInstance().isDialing + + private fun setActionBarTitle() { + val title = binding.chatToolbar.findViewById(R.id.chat_toolbar_title) + viewThemeUtils.platform.colorTextView(title, ColorRole.ON_SURFACE) + + title.text = + if (isChatThread()) { + conversationThreadInfo?.thread?.title + } else if (currentConversation?.displayName != null) { + try { + EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString() + } catch (e: java.lang.IllegalStateException) { + Log.e(TAG, "setActionBarTitle failed $e") + currentConversation?.displayName + } + } else { + "" + } + + if (isChatThread()) { + val replyAmount = conversationThreadInfo?.thread?.numReplies ?: 0 + val repliesAmountTitle = resources.getQuantityString( + R.plurals.thread_replies, + replyAmount, + replyAmount + ) + + statusMessageViewContents(repliesAmountTitle) + } else if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + var statusMessage = "" + if (currentConversation?.statusIcon != null) { + statusMessage += currentConversation?.statusIcon + } + if (currentConversation?.statusMessage != null) { + statusMessage += currentConversation?.statusMessage + } + statusMessageViewContents(statusMessage) + } else { + if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL || + currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL + ) { + var descriptionMessage = "" + descriptionMessage += currentConversation?.description + statusMessageViewContents(descriptionMessage) + } + } + } + + private fun statusMessageViewContents(statusMessageContent: String) { + val statusMessageView = binding.chatToolbar.findViewById(R.id.chat_toolbar_status_message) + if (statusMessageContent.isNotEmpty()) { + viewThemeUtils.platform.colorTextView(statusMessageView, ColorRole.ON_SURFACE) + statusMessageView.text = statusMessageContent + statusMessageView.visibility = View.VISIBLE + } else { + statusMessageView.visibility = View.GONE + } + } + + public override fun onDestroy() { + super.onDestroy() + logConversationInfos("onDestroy") + + findViewById(R.id.toolbar)?.setOnClickListener(null) + + if (actionBar != null) { + actionBar?.setIcon(null) + } + + adapter = null + disposables.dispose() + } + + private fun joinRoomWithPassword() { + // if ApplicationWideCurrentRoomHolder contains a session (because a call is active), then keep the sessionId + if (ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken == + currentConversation!!.token + ) { + sessionIdAfterRoomJoined = ApplicationWideCurrentRoomHolder.getInstance().session + + ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = roomToken + ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser + } + + if (!validSessionId()) { + Log.d(TAG, "sessionID was not valid -> joinRoom") + val startNanoTime = System.nanoTime() + Log.d(TAG, "joinRoomWithPassword - joinRoom - calling: $startNanoTime") + + chatViewModel.joinRoom(conversationUser!!, roomToken, roomPassword) + } else { + Log.d(TAG, "sessionID was valid -> skip joinRoom") + + setupWebsocket() + } + } + + fun leaveRoom(funToCallWhenLeaveSuccessful: (() -> Unit)?) { + logConversationInfos("leaveRoom") + + var apiVersion = 1 + // FIXME Fix API checking with guests? + if (conversationUser != null) { + apiVersion = ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1)) + } + + val startNanoTime = System.nanoTime() + Log.d(TAG, "leaveRoom - leaveRoom - calling: $startNanoTime") + chatViewModel.leaveRoom( + credentials!!, + ApiUtils.getUrlForParticipantsActive( + apiVersion, + conversationUser?.baseUrl!!, + roomToken + ), + funToCallWhenLeaveSuccessful + ) + } + + private fun setupWebsocket() { + if (currentConversation == null || conversationUser == null) { + return + } + + if (currentConversation!!.remoteServer?.isNotEmpty() == true) { + val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V3, 2, 1)) + ncApi.getSignalingSettings( + credentials, + ApiUtils.getUrlForSignalingSettings(apiVersion, conversationUser!!.baseUrl, roomToken) + ) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + + override fun onNext(signalingSettingsOverall: SignalingSettingsOverall) { + if (signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer == null || + signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer?.isEmpty() == true + ) { + return + } + + externalSignalingServer = ExternalSignalingServer() + externalSignalingServer!!.externalSignalingServer = signalingSettingsOverall.ocs!!.settings!! + .externalSignalingServer + externalSignalingServer!!.externalSignalingTicket = signalingSettingsOverall.ocs!!.settings!! + .externalSignalingTicket + externalSignalingServer!!.federation = signalingSettingsOverall.ocs!!.settings!!.federation + + webSocketInstance = WebSocketConnectionHelper.getExternalSignalingInstanceForServer( + externalSignalingServer!!.externalSignalingServer, + conversationUser, + externalSignalingServer!!.externalSignalingTicket, + TextUtils.isEmpty(credentials) + ) + + if (webSocketInstance != null) { + webSocketInstance?.joinRoomWithRoomTokenAndSession( + roomToken, + sessionIdAfterRoomJoined, + externalSignalingServer?.federation + ) + } + + signalingMessageSender = webSocketInstance?.signalingMessageSender + webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener) + webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener) + } + + override fun onError(e: Throwable) { + Log.e(TAG, e.message, e) + } + + override fun onComplete() { + // unused atm + } + }) + } else { + webSocketInstance = WebSocketConnectionHelper.getWebSocketInstanceForUser(conversationUser!!) + + if (webSocketInstance != null) { + webSocketInstance?.joinRoomWithRoomTokenAndSession( + roomToken, + sessionIdAfterRoomJoined, + null + ) + + signalingMessageSender = webSocketInstance?.signalingMessageSender + webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener) + webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener) + } else { + Log.d(TAG, "webSocketInstance not set up. This is only expected when not using the HPB") + } + } + } + + private fun processCallStartedMessages() { + try { + val mostRecentCallSystemMessage = adapter?.items?.first { + it.item is ChatMessage && + (it.item as ChatMessage).systemMessageType in + listOf( + ChatMessage.SystemMessageType.CALL_STARTED, + ChatMessage.SystemMessageType.CALL_JOINED, + ChatMessage.SystemMessageType.CALL_LEFT, + ChatMessage.SystemMessageType.CALL_ENDED, + ChatMessage.SystemMessageType.CALL_TRIED, + ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE, + ChatMessage.SystemMessageType.CALL_MISSED + ) + }?.item + + if (mostRecentCallSystemMessage != null) { + processMostRecentMessage( + mostRecentCallSystemMessage as ChatMessage + ) + } + } catch (e: NoSuchElementException) { + Log.d(TAG, "No System messages found $e") + } + } + + private fun processExpiredMessages() { + @SuppressLint("NotifyDataSetChanged") + fun deleteExpiredMessages() { + val messagesToDelete: ArrayList = ArrayList() + val systemTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + + if (adapter?.items != null) { + for (itemWrapper in adapter?.items!!) { + if (itemWrapper.item is ChatMessage) { + val chatMessage = itemWrapper.item as ChatMessage + if (chatMessage.expirationTimestamp != 0 && chatMessage.expirationTimestamp < systemTime) { + messagesToDelete.add(chatMessage) + } + } + } + adapter!!.delete(messagesToDelete) + adapter!!.notifyDataSetChanged() + } + } + + if (this::spreedCapabilities.isInitialized) { + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) { + deleteExpiredMessages() + } + } else { + Log.w(TAG, "spreedCapabilities are not initialized in processExpiredMessages()") + } + } + + private fun updateReadStatusOfAllMessages(xChatLastCommonRead: Int?) { + if (adapter != null) { + for (message in adapter!!.items) { + xChatLastCommonRead?.let { + updateReadStatusOfMessage(message, it) + } + } + adapter!!.notifyDataSetChanged() + } + } + + private fun updateReadStatusOfMessage( + message: MessagesListAdapter.Wrapper, + xChatLastCommonRead: Int + ) { + if (message.item is ChatMessage) { + val chatMessage = message.item as ChatMessage + if (chatMessage.jsonMessageId <= xChatLastCommonRead) { + chatMessage.readStatus = ReadStatus.READ + } else { + chatMessage.readStatus = ReadStatus.SENT + } + } + } + + private fun processMessagesFromTheFuture(chatMessageList: List, setUnreadMessagesMarker: Boolean) { + binding.scrollDownButton.visibility = View.GONE + + val scrollToBottom: Boolean + + if (setUnreadMessagesMarker) { + scrollToBottom = false + setUnreadMessageMarker(chatMessageList) + } else { + if (isScrolledToBottom()) { + scrollToBottom = true + } else { + scrollToBottom = false + binding.unreadMessagesPopup.visibility = View.VISIBLE + // here we have the problem that the chat jumps for every update + } + } + + for (chatMessage in chatMessageList) { + chatMessage.activeUser = conversationUser + + adapter?.let { + val previousChatMessage = it.items?.getOrNull(1)?.item + if (previousChatMessage != null && previousChatMessage is ChatMessage) { + chatMessage.isGrouped = groupMessages(chatMessage, previousChatMessage) + } + chatMessage.isOneToOneConversation = + (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) + chatMessage.isFormerOneToOneConversation = + (currentConversation?.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE) + Log.d(TAG, "chatMessage to add:" + chatMessage.message) + it.addToStart(chatMessage, scrollToBottom) + } + } + + // workaround to jump back to unread messages marker + if (setUnreadMessagesMarker) { + scrollToFirstUnreadMessage() + } + } + + private fun isScrolledToBottom(): Boolean { + val position = layoutManager?.findFirstVisibleItemPosition() + if (position == -1) { + Log.w( + TAG, + "FirstVisibleItemPosition was -1 but true is returned for isScrolledToBottom(). This can " + + "happen when the UI is not yet ready" + ) + return true + } + + return layoutManager?.findFirstVisibleItemPosition() == 0 + } + + private fun setUnreadMessageMarker(chatMessageList: List) { + if (chatMessageList.isNotEmpty()) { + val unreadChatMessage = ChatMessage() + unreadChatMessage.jsonMessageId = UNREAD_MESSAGES_MARKER_ID + unreadChatMessage.actorId = "-1" + unreadChatMessage.timestamp = chatMessageList[0].timestamp + unreadChatMessage.message = context.getString(R.string.nc_new_messages) + adapter?.addToStart(unreadChatMessage, false) + } + } + + private fun processMessagesNotFromTheFuture(chatMessageList: List) { + for (i in chatMessageList.indices) { + if (chatMessageList.size > i + 1) { + chatMessageList[i].isGrouped = groupMessages(chatMessageList[i], chatMessageList[i + 1]) + } + + val chatMessage = chatMessageList[i] + chatMessage.isOneToOneConversation = + currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + chatMessage.isFormerOneToOneConversation = + (currentConversation?.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE) + chatMessage.activeUser = conversationUser + chatMessage.token = roomToken + } + + if (adapter != null) { + adapter?.addToEnd(chatMessageList, false) + } + scrollToRequestedMessageIfNeeded() + } + + private fun scrollToFirstUnreadMessage() { + adapter?.let { + scrollToAndCenterMessageWithId(UNREAD_MESSAGES_MARKER_ID.toString()) + } + } + + private fun groupMessages(message1: ChatMessage, message2: ChatMessage): Boolean { + val message1IsSystem = message1.systemMessage.isNotEmpty() + val message2IsSystem = message2.systemMessage.isNotEmpty() + if (message1IsSystem != message2IsSystem) { + return false + } + + if (message1.actorType == "bots" && message1.actorId != "changelog") { + return false + } + + if (!message1IsSystem && + ( + (message1.actorType != message2.actorType) || + (message2.actorId != message1.actorId) + ) + ) { + return false + } + + val timeDifference = dateUtils.getTimeDifferenceInSeconds( + message2.timestamp, + message1.timestamp + ) + val isLessThan5Min = timeDifference > FIVE_MINUTES_IN_SECONDS + return isSameDayMessages(message2, message1) && + (message2.actorId == message1.actorId) && + (!isLessThan5Min) && + (message2.lastEditTimestamp == 0L || message1.lastEditTimestamp == 0L) + } + + private fun determinePreviousMessageIds(chatMessageList: List) { + var previousMessageId = NO_PREVIOUS_MESSAGE_ID + for (i in chatMessageList.indices.reversed()) { + val chatMessage = chatMessageList[i] + + if (previousMessageId > NO_PREVIOUS_MESSAGE_ID) { + chatMessage.previousMessageId = previousMessageId + } else { + adapter?.let { + if (!it.isEmpty) { + if (it.items[0].item is ChatMessage) { + chatMessage.previousMessageId = (it.items[0].item as ChatMessage).jsonMessageId + } else if (it.items.size > 1 && it.items[1].item is ChatMessage) { + chatMessage.previousMessageId = (it.items[1].item as ChatMessage).jsonMessageId + } + } + } + } + + previousMessageId = chatMessage.jsonMessageId + } + } + + private fun getItemFromAdapter(messageId: String): Pair? { + if (adapter != null) { + val messagePosition = adapter!!.items!!.indexOfFirst { + it.item is ChatMessage && (it.item as ChatMessage).id == messageId + } + if (messagePosition >= 0) { + val currentItem = adapter?.items?.get(messagePosition)?.item + if (currentItem is ChatMessage && currentItem.id == messageId) { + return Pair(currentItem, messagePosition) + } else { + Log.d(TAG, "currentItem retrieved was not chatmessage or its id was not correct") + } + } else { + Log.d(TAG, "messagePosition is -1, adapter # of items: " + adapter!!.itemCount) + } + } else { + Log.d(TAG, "TalkMessagesListAdapter is null") + } + return null + } + + private fun scrollToRequestedMessageIfNeeded() { + intent.getStringExtra(BundleKeys.KEY_MESSAGE_ID)?.let { + scrollToMessageWithId(it) + } + } + + private fun isSameDayNonSystemMessages(messageLeft: ChatMessage, messageRight: ChatMessage): Boolean = + TextUtils.isEmpty(messageLeft.systemMessage) && + TextUtils.isEmpty(messageRight.systemMessage) && + DateFormatter.isSameDay(messageLeft.createdAt, messageRight.createdAt) + + private fun isSameDayMessages(message1: ChatMessage, message2: ChatMessage): Boolean = + DateFormatter.isSameDay(message1.createdAt, message2.createdAt) + + override fun onLoadMore(page: Int, totalItemsCount: Int) { + val messageId = ( + adapter?.items + ?.lastOrNull { it.item is ChatMessage } + ?.item as? ChatMessage + )?.jsonMessageId + + messageId?.let { + val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken) + + chatViewModel.loadMoreMessages( + beforeMessageId = it.toLong(), + withUrl = urlForChatting, + withCredentials = credentials!!, + withMessageLimit = MESSAGE_PULL_LIMIT, + roomToken = currentConversation!!.token + ) + } + } + + override fun format(date: Date): String = + if (DateFormatter.isToday(date)) { + resources!!.getString(R.string.nc_date_header_today) + } else if (DateFormatter.isYesterday(date)) { + resources!!.getString(R.string.nc_date_header_yesterday) + } else { + DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.menu_conversation, menu) + chatMenu = menu + + if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) { + eventConversationMenuItem = menu.findItem(R.id.conversation_event) + } else { + menu.removeItem(R.id.conversation_event) + } + + if (conversationUser?.userId == "?") { + menu.removeItem(R.id.conversation_info) + } else { + loadAvatarForStatusBar() + setActionBarTitle() + } + return true + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + super.onPrepareOptionsMenu(menu) + + if (this::spreedCapabilities.isInitialized) { + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS)) { + checkShowCallButtons() + } + + val searchItem = menu.findItem(R.id.conversation_search) + searchItem.isVisible = CapabilitiesUtil.isUnifiedSearchAvailable(spreedCapabilities) && + currentConversation!!.remoteServer.isNullOrEmpty() && + !isChatThread() + + val sharedItemsItem = menu.findItem(R.id.shared_items) + sharedItemsItem.isVisible = !isChatThread() + + val conversationInfoItem = menu.findItem(R.id.conversation_info) + conversationInfoItem.isVisible = !isChatThread() + + val showThreadsItem = menu.findItem(R.id.show_threads) + showThreadsItem.isVisible = !isChatThread() && + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS) + + if (CapabilitiesUtil.isAbleToCall(spreedCapabilities) && !isChatThread()) { + conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call) + conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call) + + this.lifecycleScope.launch { + networkMonitor.isOnline.onEach { isOnline -> + conversationVoiceCallMenuItem?.isVisible = isOnline + searchItem?.isVisible = isOnline + conversationVideoMenuItem?.isVisible = isOnline + }.collect() + } + + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) { + Handler().post { + findViewById(R.id.conversation_voice_call)?.setOnLongClickListener { + showCallButtonMenu(true) + true + } + } + + Handler().post { + findViewById(R.id.conversation_video_call)?.setOnLongClickListener { + showCallButtonMenu(false) + true + } + } + } + } else { + menu.removeItem(R.id.conversation_video_call) + menu.removeItem(R.id.conversation_voice_call) + } + + handleThreadNotificationIcon(menu.findItem(R.id.thread_notifications)) + } + return true + } + + private fun handleThreadNotificationIcon(threadNotificationItem: MenuItem) { + threadNotificationItem.isVisible = isChatThread() && + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS) + + val threadNotificationIcon = when (conversationThreadInfo?.attendee?.notificationLevel) { + 1 -> R.drawable.outline_notifications_active_24 + 3 -> R.drawable.ic_baseline_notifications_off_24 + else -> R.drawable.baseline_notifications_24 + } + threadNotificationItem.icon = ContextCompat.getDrawable(context, threadNotificationIcon) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + R.id.conversation_video_call -> { + startACall(false, false) + true + } + + R.id.conversation_voice_call -> { + startACall(true, false) + true + } + + R.id.conversation_info -> { + showConversationInfoScreen() + true + } + + R.id.shared_items -> { + showSharedItems() + true + } + + R.id.conversation_search -> { + startMessageSearch() + true + } + + R.id.conversation_event -> { + val anchorView = findViewById(R.id.conversation_event) + showConversationEventMenu(anchorView) + true + } + + R.id.show_threads -> { + openThreadsOverview() + true + } + + R.id.thread_notifications -> { + showThreadNotificationMenu() + true + } + + else -> super.onOptionsItemSelected(item) + } + + @Suppress("Detekt.LongMethod") + private fun showThreadNotificationMenu() { + fun setThreadNotificationLevel(level: Int) { + val threadNotificationUrl = ApiUtils.getUrlForThreadNotificationLevel( + version = 1, + baseUrl = conversationUser!!.baseUrl, + token = roomToken, + threadId = conversationThreadId!!.toInt() + ) + chatViewModel.setThreadNotificationLevel(credentials!!, threadNotificationUrl, level) + } + + if (overflowMenuHostView == null) { + val threadNotificationsAnchor: View? = findViewById(R.id.thread_notifications) + + val colorScheme = viewThemeUtils.getColorScheme(this) + + overflowMenuHostView = ComposeView(this).apply { + setContent { + MaterialTheme( + colorScheme = colorScheme + ) { + val items = listOf( + MenuItemData( + title = context.resources.getString(R.string.notifications_default), + subtitle = context.resources.getString( + R.string.notifications_default_description + ), + icon = R.drawable.baseline_notifications_24, + onClick = { + setThreadNotificationLevel(0) + } + ), + MenuItemData( + title = context.resources.getString(R.string.notification_all_messages), + subtitle = null, + icon = R.drawable.outline_notifications_active_24, + onClick = { + setThreadNotificationLevel(1) + } + ), + MenuItemData( + title = context.resources.getString(R.string.notification_mention_only), + subtitle = null, + icon = R.drawable.baseline_notifications_24, + onClick = { + setThreadNotificationLevel(2) + } + ), + MenuItemData( + title = context.resources.getString(R.string.notification_off), + subtitle = null, + icon = R.drawable.ic_baseline_notifications_off_24, + onClick = { + setThreadNotificationLevel(3) + } + ) + ) + + OverflowMenu( + anchor = threadNotificationsAnchor, + expanded = isThreadMenuExpanded, + items = items, + onDismiss = { isThreadMenuExpanded = false } + ) + } + } + } + + addContentView( + overflowMenuHostView, + CoordinatorLayout.LayoutParams( + CoordinatorLayout.LayoutParams.MATCH_PARENT, + CoordinatorLayout.LayoutParams.MATCH_PARENT + ) + ) + } + isThreadMenuExpanded = true + } + + @SuppressLint("InflateParams") + private fun showConversationEventMenu(anchorView: View) { + val popupView = layoutInflater.inflate(R.layout.item_event_schedule, null) + + val subtitleTextView = popupView.findViewById(R.id.meetingTime) + + val popupWindow = PopupWindow( + popupView, + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + true + ) + + popupWindow.isOutsideTouchable = true + popupWindow.isFocusable = true + popupWindow.showAsDropDown(anchorView, 0, -anchorView.height) + + val meetingStatus = showEventSchedule() + subtitleTextView.text = meetingStatus + + deleteEventConversation(meetingStatus, popupWindow, popupView) + archiveEventConversation(meetingStatus, popupWindow, popupView) + } + + private fun deleteEventConversation(meetingStatus: String, popupWindow: PopupWindow, popupView: View) { + val deleteConversation = popupView.findViewById(R.id.delete_conversation) + if (meetingStatus == context.resources.getString(R.string.nc_meeting_ended) && + currentConversation?.canDeleteConversation == true + ) { + deleteConversation.visibility = View.VISIBLE + + deleteConversation.setOnClickListener { + deleteConversationDialog(it.context) + popupWindow.dismiss() + } + } else { + deleteConversation.visibility = View.GONE + } + } + + private fun archiveEventConversation(meetingStatus: String, popupWindow: PopupWindow, popupView: View) { + val archiveConversation = popupView.findViewById(R.id.archive_conversation) + val unarchiveConversation = popupView.findViewById(R.id.unarchive_conversation) + if (meetingStatus == context.resources.getString(R.string.nc_meeting_ended) && + ( + Participant.ParticipantType.MODERATOR == currentConversation?.participantType || + Participant.ParticipantType.OWNER == currentConversation?.participantType + ) + ) { + if (currentConversation?.hasArchived == false) { + unarchiveConversation.visibility = View.GONE + archiveConversation.visibility = View.VISIBLE + archiveConversation.setOnClickListener { + this.lifecycleScope.launch { + conversationInfoViewModel.archiveConversation(conversationUser!!, currentConversation?.token!!) + Snackbar.make( + binding.root, + String.format( + context.resources.getString(R.string.archived_conversation), + currentConversation?.displayName + ), + Snackbar.LENGTH_LONG + ).show() + } + popupWindow.dismiss() + } + } else { + unarchiveConversation.visibility = View.VISIBLE + archiveConversation.visibility = View.GONE + unarchiveConversation.setOnClickListener { + this.lifecycleScope.launch { + conversationInfoViewModel.unarchiveConversation( + conversationUser!!, + currentConversation?.token!! + ) + Snackbar.make( + binding.root, + String.format( + context.resources.getString(R.string.unarchived_conversation), + currentConversation?.displayName + ), + Snackbar.LENGTH_LONG + ).show() + } + popupWindow.dismiss() + } + } + } else { + archiveConversation.visibility = View.GONE + unarchiveConversation.visibility = View.GONE + } + } + + private fun deleteConversation(conversation: ConversationModel) { + val data = Data.Builder() + data.putLong( + KEY_INTERNAL_USER_ID, + conversationUser?.id!! + ) + data.putString(KEY_ROOM_TOKEN, conversation.token) + + val deleteConversationWorker = + OneTimeWorkRequest.Builder(DeleteConversationWorker::class.java).setInputData(data.build()).build() + WorkManager.getInstance().enqueue(deleteConversationWorker) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(deleteConversationWorker.id) + .observeForever { workInfo: WorkInfo? -> + if (workInfo != null) { + when (workInfo.state) { + WorkInfo.State.SUCCEEDED -> { + val successMessage = String.format( + context.resources.getString(R.string.deleted_conversation), + conversation.displayName + ) + Snackbar.make(binding.root, successMessage, Snackbar.LENGTH_LONG).show() + finish() + } + + WorkInfo.State.FAILED -> { + val errorMessage = context.resources.getString(R.string.nc_common_error_sorry) + Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_LONG).show() + } + + else -> { + } + } + } + } + } + + private fun showEventSchedule(): String { + val meetingTimeStamp = currentConversation?.objectId ?: "" + val status = getMeetingSchedule(meetingTimeStamp) + return status + } + + private fun getMeetingSchedule(meetingTimeStamp: String): String { + val timestamps = meetingTimeStamp.split("#") + if (timestamps.size != 2) return context.resources.getString(R.string.nc_invalid_time) + + val startEpoch = timestamps[ZERO_INDEX].toLong() + val endEpoch = timestamps[ONE_INDEX].toLong() + + val startDateTime = Instant.ofEpochSecond(startEpoch).atZone(ZoneId.systemDefault()) + val endDateTime = Instant.ofEpochSecond(endEpoch).atZone(ZoneId.systemDefault()) + val currentTime = ZonedDateTime.now(ZoneId.systemDefault()) + + return when { + currentTime.isBefore(startDateTime) -> { + val isToday = startDateTime.toLocalDate().isEqual(currentTime.toLocalDate()) + val isTomorrow = startDateTime.toLocalDate().isEqual(currentTime.toLocalDate().plusDays(1)) + when { + isToday -> String.format( + context.resources.getString(R.string.nc_today_meeting), + startDateTime.format(DateTimeFormatter.ofPattern("HH:mm")) + ) + + isTomorrow -> String.format( + context.resources.getString(R.string.nc_tomorrow_meeting), + startDateTime.format(DateTimeFormatter.ofPattern("HH:mm")) + ) + + else -> startDateTime.format(DateTimeFormatter.ofPattern("MMM d, yyyy, HH:mm")) + } + } + + currentTime.isAfter(endDateTime) -> context.resources.getString(R.string.nc_meeting_ended) + else -> context.resources.getString(R.string.nc_ongoing_meeting) + } + } + + private fun showSharedItems() { + val intent = Intent(this, SharedItemsActivity::class.java) + intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName) + intent.putExtra(KEY_ROOM_TOKEN, roomToken) + intent.putExtra( + SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR, + ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!) + ) + startActivity(intent) + } + + private fun startMessageSearch() { + val intent = Intent(this, MessageSearchActivity::class.java) + intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName) + intent.putExtra(KEY_ROOM_TOKEN, roomToken) + startMessageSearchForResult.launch(intent) + } + + private fun handleSystemMessages(chatMessageList: List): List { + val chatMessageMap = chatMessageList.associateBy { it.id }.toMutableMap() + + val chatMessageIterator = chatMessageMap.iterator() + while (chatMessageIterator.hasNext()) { + val currentMessage = chatMessageIterator.next() + + if (isInfoMessageAboutDeletion(currentMessage) || + isReactionsMessage(currentMessage) || + isPollVotedMessage(currentMessage) || + isEditMessage(currentMessage) || + isThreadCreatedMessage(currentMessage) + ) { + chatMessageIterator.remove() + } + } + return chatMessageMap.values.toList() + } + + private fun handleExpandableSystemMessages(chatMessageList: List): List { + val chatMessageMap = chatMessageList.associateBy { it.id }.toMutableMap() + val chatMessageIterator = chatMessageMap.iterator() + + while (chatMessageIterator.hasNext()) { + val currentMessage = chatMessageIterator.next() + chatMessageMap[currentMessage.value.previousMessageId.toString()]?.let { previousMessage -> + if (isSystemMessage(currentMessage.value) && + previousMessage.systemMessageType == currentMessage.value.systemMessageType && + isSameDayMessages(previousMessage, currentMessage.value) + ) { + groupSystemMessages(previousMessage, currentMessage.value) + } + } + } + return chatMessageMap.values.toList() + } + + private fun groupSystemMessages(previousMessage: ChatMessage, currentMessage: ChatMessage) { + previousMessage.expandableParent = true + currentMessage.expandableParent = false + + if (currentMessage.lastItemOfExpandableGroup == 0) { + currentMessage.lastItemOfExpandableGroup = currentMessage.jsonMessageId + } + + previousMessage.lastItemOfExpandableGroup = currentMessage.lastItemOfExpandableGroup + previousMessage.expandableChildrenAmount = currentMessage.expandableChildrenAmount + 1 + } + + private fun handleThreadMessages(chatMessageList: List): List { + fun isThreadChildMessage(currentMessage: MutableMap.MutableEntry): Boolean = + currentMessage.value.isThread && + currentMessage.value.threadId?.toInt() != currentMessage.value.jsonMessageId + + val chatMessageMap = chatMessageList.associateBy { it.id }.toMutableMap() + + if (conversationThreadId == null) { + val chatMessageIterator = chatMessageMap.iterator() + while (chatMessageIterator.hasNext()) { + val currentMessage = chatMessageIterator.next() + + if (isThreadChildMessage(currentMessage)) { + chatMessageIterator.remove() + } + } + } + + return chatMessageMap.values.toList() + } + + private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry): Boolean = + currentMessage.value.parentMessageId != null && + currentMessage.value.systemMessageType == ChatMessage + .SystemMessageType.MESSAGE_DELETED + + private fun isReactionsMessage(currentMessage: MutableMap.MutableEntry): Boolean = + currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION || + currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED || + currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED + + private fun isThreadCreatedMessage(currentMessage: MutableMap.MutableEntry): Boolean = + currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.THREAD_CREATED + + private fun isEditMessage(currentMessage: MutableMap.MutableEntry): Boolean = + currentMessage.value.parentMessageId != null && + currentMessage.value.systemMessageType == ChatMessage + .SystemMessageType.MESSAGE_EDITED + + private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry): Boolean = + currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED + + private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) { + currentConversation?.let { + if (conversationUser != null) { + val pp = ParticipantPermissions(spreedCapabilities, it) + if (!pp.canStartCall() && currentConversation?.hasCall == false) { + Snackbar.make(binding.root, R.string.startCallForbidden, Snackbar.LENGTH_LONG).show() + } else { + ApplicationWideCurrentRoomHolder.getInstance().isDialing = true + val callIntent = getIntentForCall(isVoiceOnlyCall, callWithoutNotification) + if (callIntent != null) { + startActivity(callIntent) + } + } + } + } + } + + private fun getIntentForCall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean): Intent? { + currentConversation?.let { + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, roomToken) + bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword) + bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl!!) + bundle.putString(KEY_CONVERSATION_NAME, it.displayName) + bundle.putInt(KEY_RECORDING_STATE, it.callRecording) + bundle.putBoolean(KEY_IS_MODERATOR, ConversationUtils.isParticipantOwnerOrModerator(it)) + bundle.putBoolean( + BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO, + participantPermissions.canPublishAudio() + ) + bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation()) + bundle.putBoolean( + BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO, + participantPermissions.canPublishVideo() + ) + + if (isVoiceOnlyCall) { + bundle.putBoolean(KEY_CALL_VOICE_ONLY, true) + } + if (callWithoutNotification) { + bundle.putBoolean(BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION, true) + } + + if (it.objectType == ConversationEnums.ObjectType.ROOM) { + bundle.putBoolean(KEY_IS_BREAKOUT_ROOM, true) + } + + val callIntent = Intent(this, CallActivity::class.java) + callIntent.putExtras(bundle) + return callIntent + } ?: run { + return null + } + } + + override fun onClickReaction(chatMessage: ChatMessage, emoji: String) { + VibrationUtils.vibrateShort(context) + if (chatMessage.reactionsSelf?.contains(emoji) == true) { + chatViewModel.deleteReaction(roomToken, chatMessage, emoji) + } else { + chatViewModel.addReaction(roomToken, chatMessage, emoji) + } + } + + override fun openThread(chatMessage: ChatMessage) { + openThread(chatMessage.jsonMessageId.toLong()) + } + + override fun onLongClickReactions(chatMessage: ChatMessage) { + ShowReactionsDialog( + this, + roomToken, + chatMessage, + conversationUser, + participantPermissions.hasChatPermission(), + ncApi + ).show() + } + + override fun onOpenMessageActionsDialog(chatMessage: ChatMessage) { + openMessageActionsDialog(chatMessage) + } + + override fun onMessageViewLongClick(view: View?, message: IMessage?) { + openMessageActionsDialog(message) + } + + override fun onPreviewMessageLongClick(chatMessage: ChatMessage) { + onOpenMessageActionsDialog(chatMessage) + } + + private fun openMessageActionsDialog(iMessage: IMessage?) { + val message = iMessage as ChatMessage + + if (message.isTemporary) { + TempMessageActionsDialog( + this, + message + ).show() + } else if (hasVisibleItems(message) && + !isSystemMessage(message) + ) { + MessageActionsDialog( + this, + message, + conversationUser, + currentConversation, + isShowMessageDeletionButton(message), + participantPermissions.hasChatPermission(), + spreedCapabilities + ).show() + } + } + + private fun isSystemMessage(message: ChatMessage): Boolean = + ChatMessage.MessageType.SYSTEM_MESSAGE == message.getCalculateMessageType() + + fun deleteMessage(message: IMessage) { + if (!participantPermissions.hasChatPermission()) { + Log.w( + TAG, + "Deletion of message is skipped because of restrictions by permissions. " + + "This method should not have been called!" + ) + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } else { + var apiVersion = 1 + // FIXME Fix API checking with guests? + if (conversationUser != null) { + apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1)) + } + + chatViewModel.deleteChatMessages( + credentials!!, + ApiUtils.getUrlForChatMessage( + apiVersion, + conversationUser?.baseUrl!!, + roomToken, + message.id!! + ), + message.id!! + ) + } + } + + fun replyPrivately(message: IMessage?) { + val apiVersion = + ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1)) + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + version = apiVersion, + baseUrl = conversationUser?.baseUrl!!, + roomType = "1", + invite = message?.user?.id?.substring(INVITE_LENGTH) + ) + chatViewModel.createRoom( + credentials!!, + retrofitBucket.url!!, + retrofitBucket.queryMap!! + ) + } + + fun forwardMessage(message: IMessage?) { + val bundle = Bundle() + bundle.putBoolean(BundleKeys.KEY_FORWARD_MSG_FLAG, true) + bundle.putString(BundleKeys.KEY_FORWARD_MSG_TEXT, message?.text) + bundle.putString(BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM, roomToken) + + val intent = Intent(this, ConversationsListActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } + + fun remindMeLater(message: ChatMessage?) { + Log.d(TAG, "remindMeLater called") + + val chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(ApiUtils.API_V1, 1)) + + val bundle = bundleOf() + bundle.putString(KEY_ROOM_TOKEN, roomToken) + bundle.putString(BundleKeys.KEY_MESSAGE_ID, message!!.id) + bundle.putInt(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion) + + binding.genericComposeView.apply { + val shouldDismiss = mutableStateOf(false) + setContent { + DateTimeCompose(bundle).GetDateTimeDialog(shouldDismiss, this@ChatActivity) + } + } + } + + fun markAsUnread(message: IMessage?) { + val chatMessage = message as ChatMessage? + if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) { + chatViewModel.setChatReadMarker( + credentials!!, + ApiUtils.getUrlForChatReadMarker( + ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(ApiUtils.API_V1)), + conversationUser?.baseUrl!!, + roomToken + ), + chatMessage.previousMessageId + ) + } + } + + fun copyMessage(message: IMessage?) { + val clipboardManager = + getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText( + resources?.getString(R.string.nc_app_product_name), + message?.text + ) + clipboardManager.setPrimaryClip(clipData) + } + + fun translateMessage(message: IMessage?) { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_TRANSLATE_MESSAGE, message?.text) + + val intent = Intent(this, TranslateActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } + + fun share(message: ChatMessage) { + val filename = message.selectedIndividualHashMap!!["name"] + path = applicationContext.cacheDir.absolutePath + "/" + filename + val shareUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID, + File(path) + ) + + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, shareUri) + type = Mimetype.IMAGE_PREFIX_GENERIC + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to))) + } + + fun checkIfSharable(message: ChatMessage) { + val filename = message.selectedIndividualHashMap!!["name"] + path = applicationContext.cacheDir.absolutePath + "/" + filename + val file = File(context.cacheDir, filename!!) + if (file.exists()) { + share(message) + } else { + downloadFileToCache(message, false) { + share(message) + } + } + } + + private fun showSaveToStorageWarning(message: ChatMessage) { + val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance( + message.selectedIndividualHashMap!!["name"]!! + ) + saveFragment.show( + supportFragmentManager, + SaveToStorageDialogFragment.TAG + ) + } + + fun checkIfSaveable(message: ChatMessage) { + val filename = message.selectedIndividualHashMap!!["name"] + path = applicationContext.cacheDir.absolutePath + "/" + filename + val file = File(context.cacheDir, filename!!) + if (file.exists()) { + showSaveToStorageWarning(message) + } else { + downloadFileToCache(message, false) { + showSaveToStorageWarning(message) + } + } + } + + fun shareToNotes(message: ChatMessage) { + val apiVersion = ApiUtils.getConversationApiVersion( + conversationUser!!, + intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1) + ) + + this.lifecycleScope.launch { + val noteToSelfConversation = chatViewModel.checkForNoteToSelf( + ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)!!, + ApiUtils.getUrlForNoteToSelf( + apiVersion, + conversationUser!!.baseUrl + ) + ) + + if (noteToSelfConversation != null) { + var shareUri: Uri? = null + val data: HashMap? + var metaData = "" + var objectId = "" + if (message.hasFileAttachment()) { + val filename = message.selectedIndividualHashMap!!["name"] + path = applicationContext.cacheDir.absolutePath + "/" + filename + shareUri = FileProvider.getUriForFile( + context, + BuildConfig.APPLICATION_ID, + File(path) + ) + + grantUriPermission( + applicationContext.packageName, + shareUri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } else if (message.hasGeoLocation()) { + data = message.messageParameters?.get("object") + objectId = data?.get("id")!! + val name = data["name"]!! + val lat = data["latitude"]!! + val lon = data["longitude"]!! + metaData = + "{\"type\":\"geo-location\",\"id\":\"geo:$lat,$lon\",\"latitude\":\"$lat\"," + + "\"longitude\":\"$lon\",\"name\":\"$name\"}" + } + + shareToNotes(shareUri, noteToSelfConversation.token, message, objectId, metaData) + } else { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + } + } + + private fun shareToNotes( + shareUri: Uri?, + roomToken: String, + message: ChatMessage, + objectId: String, + metaData: String + ) { + val type = message.getCalculateMessageType() + when (type) { + ChatMessage.MessageType.VOICE_MESSAGE -> { + uploadFile( + shareUri.toString(), + true, + roomToken = roomToken, + caption = "", + replyToMessageId = getReplyToMessageId(), + displayName = currentConversation?.displayName ?: "" + ) + showSnackBar(roomToken) + } + + ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> { + val caption = if (message.message != "{file}") message.message else "" + if (null != shareUri) { + try { + context.contentResolver.openInputStream(shareUri)?.close() + uploadFile( + fileUri = shareUri.toString(), + isVoiceMessage = false, + caption = caption!!, + roomToken = roomToken, + replyToMessageId = getReplyToMessageId(), + displayName = currentConversation?.displayName ?: "" + ) + showSnackBar(roomToken) + } catch (e: Exception) { + Log.w(TAG, "File corresponding to the uri does not exist $shareUri", e) + downloadFileToCache(message, false) { + uploadFile( + fileUri = shareUri.toString(), + isVoiceMessage = false, + caption = caption!!, + roomToken = roomToken, + replyToMessageId = getReplyToMessageId(), + displayName = currentConversation?.displayName ?: "" + ) + showSnackBar(roomToken) + } + } + } + } + + ChatMessage.MessageType.SINGLE_NC_GEOLOCATION_MESSAGE -> { + val apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1)) + chatViewModel.shareLocationToNotes( + credentials!!, + ApiUtils.getUrlToSendLocation(apiVersion, conversationUser!!.baseUrl!!, roomToken), + "geo-location", + objectId, + metaData + ) + showSnackBar(roomToken) + } + + ChatMessage.MessageType.REGULAR_TEXT_MESSAGE -> { + val apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1)) + chatViewModel.shareToNotes( + credentials!!, + ApiUtils.getUrlForChat(apiVersion, conversationUser!!.baseUrl!!, roomToken), + message.message!!, + conversationUser!!.displayName!! + ) + showSnackBar(roomToken) + } + + else -> {} + } + } + + fun showSnackBar(roomToken: String) { + val snackBar = Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_LONG) + snackBar.view.setOnClickListener { + openNoteToSelfConversation(roomToken) + } + snackBar.show() + } + + fun openNoteToSelfConversation(noteToSelfRoomToken: String) { + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, noteToSelfRoomToken) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(chatIntent) + } + + fun openInFilesApp(message: ChatMessage) { + val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID] + val link = message.selectedIndividualHashMap!!["link"] + val fileViewerUtils = FileViewerUtils(this, message.activeUser!!) + fileViewerUtils.openFileInFilesApp(link!!, keyID!!) + } + + private fun hasVisibleItems(message: ChatMessage): Boolean = + !message.isDeleted || + // copy message + message.replyable || + // reply to + message.replyable && + // reply privately + conversationUser?.userId?.isNotEmpty() == true && + conversationUser!!.userId != "?" && + message.user.id.startsWith("users/") && + message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId && + currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || + isShowMessageDeletionButton(message) || + // delete + ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() || + // forward + message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && + // mark as unread + ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() && + BuildConfig.DEBUG + + private fun setMessageAsDeleted(message: IMessage?) { + val messageTemp = message as ChatMessage + messageTemp.isDeleted = true + messageTemp.message = getString(R.string.message_deleted_by_you) + + messageTemp.isOneToOneConversation = + currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + messageTemp.activeUser = conversationUser + + adapter?.update(messageTemp) + } + + private fun setMessageAsEdited(message: IMessage?, newString: String) { + val messageTemp = message as ChatMessage + messageTemp.lastEditTimestamp = message.lastEditTimestamp + messageTemp.message = newString + + val index = adapter?.getMessagePositionById(messageTemp.id)!! + if (index > 0) { + val adapterMsg = adapter?.items?.get(index)?.item as ChatMessage + messageTemp.parentMessageId = adapterMsg.parentMessageId + } + + messageTemp.isOneToOneConversation = + currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + messageTemp.activeUser = conversationUser + + adapter?.update(messageTemp) + } + + private fun updateMessageInsideAdapter(message: IMessage?) { + message?.let { + val messageTemp = message as ChatMessage + + // TODO is this needed? + messageTemp.isOneToOneConversation = + currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + messageTemp.activeUser = conversationUser + + adapter?.update(messageTemp) + } + } + + fun updateUiToAddReaction(message: ChatMessage, emoji: String) { + if (message.reactions == null) { + message.reactions = LinkedHashMap() + } + + if (message.reactionsSelf == null) { + message.reactionsSelf = ArrayList() + } + + var amount = message.reactions!![emoji] + if (amount == null) { + amount = 0 + } + message.reactions!![emoji] = amount + 1 + message.reactionsSelf!!.add(emoji) + adapter?.update(message) + } + + fun updateUiToDeleteReaction(message: ChatMessage, emoji: String) { + if (message.reactions == null) { + message.reactions = LinkedHashMap() + } + + if (message.reactionsSelf == null) { + message.reactionsSelf = ArrayList() + } + + var amount = message.reactions!![emoji] + if (amount == null) { + amount = 0 + } + message.reactions!![emoji] = amount - 1 + if (message.reactions!![emoji]!! <= 0) { + message.reactions!!.remove(emoji) + } + message.reactionsSelf!!.remove(emoji) + adapter?.update(message) + } + + private fun isShowMessageDeletionButton(message: ChatMessage): Boolean { + val isUserAllowedByPrivileges = userAllowedByPrivilages(message) + + val isOlderThanSixHours = message + .createdAt + .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE)) + val hasDeleteMessagesUnlimitedCapability = hasSpreedFeatureCapability( + spreedCapabilities, + SpreedFeatures.DELETE_MESSAGES_UNLIMITED + ) + + return when { + !isUserAllowedByPrivileges -> false + !hasDeleteMessagesUnlimitedCapability && isOlderThanSixHours -> false + message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false + message.isDeleted -> false + !hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false + !participantPermissions.hasChatPermission() -> false + hasDeleteMessagesUnlimitedCapability -> true + else -> true + } + } + + fun userAllowedByPrivilages(message: ChatMessage): Boolean { + if (conversationUser == null) return false + + val isUserAllowedByPrivileges = if (message.actorId == conversationUser!!.userId) { + true + } else { + ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) + } + return isUserAllowedByPrivileges + } + + override fun hasContentFor(message: ChatMessage, type: Byte): Boolean = + when (type) { + CONTENT_TYPE_LOCATION -> message.hasGeoLocation() + CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage + CONTENT_TYPE_POLL -> message.isPoll() + CONTENT_TYPE_LINK_PREVIEW -> message.isLinkPreview() + CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage) + CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == UNREAD_MESSAGES_MARKER_ID.toString() + CONTENT_TYPE_CALL_STARTED -> message.id == "-2" + CONTENT_TYPE_DECK_CARD -> message.isDeckCard() + + else -> false + } + + private fun processMostRecentMessage(recent: ChatMessage) { + when (recent.systemMessageType) { + ChatMessage.SystemMessageType.CALL_STARTED -> { + if (!callStarted) { + messageInputViewModel.showCallStartedIndicator(recent, true) + callStarted = true + } + } + + ChatMessage.SystemMessageType.CALL_ENDED, + ChatMessage.SystemMessageType.CALL_MISSED, + ChatMessage.SystemMessageType.CALL_TRIED, + ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE -> { + callStarted = false + messageInputViewModel.showCallStartedIndicator(recent, false) + } + + else -> {} + } + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) { + /* + switch (webSocketCommunicationEvent.getType()) { + case "refreshChat": + + if ( + webSocketCommunicationEvent + .getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID) + .equals(Long.toString(conversationUser.getId())) + ) { + if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) { + pullChatMessages(2); + } + } + break; + default: + }*/ + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) { + if (currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || + currentConversation?.name != userMentionClickEvent.userId + ) { + var apiVersion = 1 + // FIXME Fix API checking with guests? + if (conversationUser != null) { + apiVersion = ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1)) + } + + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + version = apiVersion, + baseUrl = conversationUser?.baseUrl!!, + roomType = "1", + invite = userMentionClickEvent.userId + ) + + chatViewModel.createRoom( + credentials!!, + retrofitBucket.url!!, + retrofitBucket.queryMap!! + ) + } + } + + fun sendPictureFromCamIntent() { + if (!permissionUtil.isCameraPermissionGranted()) { + requestCameraPermissions() + } else { + startPickCameraIntentForResult.launch(TakePhotoActivity.createIntent(context)) + } + } + + fun sendVideoFromCamIntent() { + if (!permissionUtil.isCameraPermissionGranted()) { + requestCameraPermissions() + } else { + Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent -> + takeVideoIntent.resolveActivity(packageManager)?.also { + val videoFile: File? = try { + val outputDir = context.cacheDir + val dateFormat = SimpleDateFormat(FILE_DATE_PATTERN, Locale.ROOT) + val date = dateFormat.format(Date()) + val videoName = String.format( + context.resources.getString(R.string.nc_video_filename), + date + ) + File("$outputDir/$videoName$VIDEO_SUFFIX") + } catch (e: IOException) { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Log.e(TAG, "error while creating video file", e) + null + } + + videoFile?.also { + videoURI = FileProvider.getUriForFile(context, context.packageName, it) + takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoURI) + startPickCameraIntentForResult.launch(takeVideoIntent) + } + } + } + } + } + + fun createPoll() { + val pollVoteDialog = PollCreateDialogFragment.newInstance( + roomToken + ) + pollVoteDialog.show(supportFragmentManager, TAG) + } + + fun createThread() { + messageInputViewModel.startThreadCreation() + } + + fun jumpToQuotedMessage(parentMessage: ChatMessage) { + var foundMessage = false + for (position in 0 until (adapter!!.items.size)) { + val currentItem = adapter?.items?.get(position)?.item + if (currentItem is ChatMessage && currentItem.id == parentMessage.id) { + layoutManager!!.scrollToPosition(position) + foundMessage = true + break + } + } + if (!foundMessage) { + Log.d(TAG, "quoted message with id " + parentMessage.id + " was not found in adapter") + startContextChatWindowForMessage(parentMessage.id) + } + } + + private fun isChatThread(): Boolean = conversationThreadId != null && conversationThreadId!! > 0 + + fun openThread(messageId: Long) { + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, roomToken) + bundle.putLong(KEY_THREAD_ID, messageId) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + startActivity(chatIntent) + } + + fun openThreadsOverview() { + val threadsUrl = ApiUtils.getUrlForRecentThreads( + version = 1, + baseUrl = conversationUser!!.baseUrl, + token = roomToken + ) + + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, roomToken) + bundle.putString(ThreadsOverviewActivity.KEY_APPBAR_TITLE, getString(R.string.recent_threads)) + bundle.putString(ThreadsOverviewActivity.KEY_THREADS_SOURCE_URL, threadsUrl) + + val threadsOverviewIntent = Intent(context, ThreadsOverviewActivity::class.java) + threadsOverviewIntent.putExtras(bundle) + startActivity(threadsOverviewIntent) + } + + override fun joinAudioCall() { + startACall(true, false) + } + + override fun joinVideoCall() { + startACall(false, false) + } + + private fun logConversationInfos(methodName: String) { + Log.d(TAG, " |-----------------------------------------------") + Log.d(TAG, " | method: $methodName") + Log.d(TAG, " | ChatActivity: " + System.identityHashCode(this).toString()) + Log.d(TAG, " | roomToken: $roomToken") + Log.d(TAG, " | currentConversation?.displayName: ${currentConversation?.displayName}") + Log.d(TAG, " | sessionIdAfterRoomJoined: $sessionIdAfterRoomJoined") + Log.d(TAG, " |-----------------------------------------------") + } + + fun shareMessageText(message: String) { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, message) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, getString(R.string.share)) + startActivity(shareIntent) + } + + fun joinOneToOneConversation(userId: String) { + val apiVersion = + ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1)) + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + version = apiVersion, + baseUrl = conversationUser?.baseUrl!!, + roomType = ROOM_TYPE_ONE_TO_ONE, + source = ACTOR_TYPE, + invite = userId + ) + chatViewModel.createRoom( + credentials!!, + retrofitBucket.url!!, + retrofitBucket.queryMap!! + ) + } + + fun uploadFile( + fileUri: String, + isVoiceMessage: Boolean, + caption: String = "", + roomToken: String = "", + replyToMessageId: Int? = null, + displayName: String + ) { + chatViewModel.uploadFile( + fileUri, + isVoiceMessage, + caption, + roomToken, + replyToMessageId, + displayName + ) + cancelReply() + } + + fun cancelReply() { + messageInputViewModel.reply(null) + chatViewModel.messageDraft.quotedMessageText = null + chatViewModel.messageDraft.quotedDisplayName = null + chatViewModel.messageDraft.quotedImageUrl = null + chatViewModel.messageDraft.quotedJsonId = null + } + + fun cancelCreateThread() { + chatViewModel.clearThreadTitle() + } + + companion object { + val TAG = ChatActivity::class.simpleName + private const val CONTENT_TYPE_CALL_STARTED: Byte = 1 + private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 2 + private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 3 + private const val CONTENT_TYPE_LOCATION: Byte = 4 + private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 5 + private const val CONTENT_TYPE_POLL: Byte = 6 + private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 7 + private const val CONTENT_TYPE_DECK_CARD: Byte = 8 + private const val UNREAD_MESSAGES_MARKER_ID = -1 + private const val GET_ROOM_INFO_DELAY_NORMAL: Long = 30000 + private const val GET_ROOM_INFO_DELAY_LOBBY: Long = 5000 + private const val MILLIS_250 = 250L + private const val AGE_THRESHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000) + private const val REQUEST_SHARE_FILE_PERMISSION: Int = 221 + private const val REQUEST_RECORD_AUDIO_PERMISSION = 222 + private const val REQUEST_READ_CONTACT_PERMISSION = 234 + private const val REQUEST_CAMERA_PERMISSION = 223 + private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss" + private const val VIDEO_SUFFIX = ".mp4" + private const val FULLY_OPAQUE_INT: Int = 255 + private const val SEMI_TRANSPARENT_INT: Int = 99 + private const val VOICE_MESSAGE_SEEKBAR_BASE = 1000 + private const val NO_PREVIOUS_MESSAGE_ID: Int = -1 + private const val TOOLBAR_AVATAR_RATIO = 1.5 + private const val STATUS_SIZE_IN_DP = 9f + private const val HTTP_BAD_REQUEST = 400 + private const val HTTP_FORBIDDEN = 403 + private const val HTTP_NOT_FOUND = 404 + private const val MESSAGE_PULL_LIMIT = 100 + private const val INVITE_LENGTH = 6 + private const val ACTOR_LENGTH = 6 + private const val CHUNK_SIZE: Int = 10 + private const val ONE_SECOND_IN_MILLIS = 1000 + private const val WHITESPACE = " " + private const val COMMA = ", " + private const val TYPING_INDICATOR_ANIMATION_DURATION = 200L + private const val TYPING_INDICATOR_MAX_NAME_LENGTH = 14 + private const val TYPING_INDICATOR_POSITION_VISIBLE = -18f + private const val TYPING_INDICATOR_POSITION_HIDDEN = -1f + private const val MILLISEC_15: Long = 15 + private const val CURRENT_AUDIO_MESSAGE_KEY = "CURRENT_AUDIO_MESSAGE" + private const val CURRENT_AUDIO_POSITION_KEY = "CURRENT_AUDIO_POSITION" + private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING" + private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG" + private const val FIVE_MINUTES_IN_SECONDS: Long = 300 + private const val ROOM_TYPE_ONE_TO_ONE = "1" + private const val ACTOR_TYPE = "users" + const val CONVERSATION_INTERNAL_ID = "CONVERSATION_INTERNAL_ID" + const val NO_OFFLINE_MESSAGES_FOUND = "NO_OFFLINE_MESSAGES_FOUND" + const val VOICE_MESSAGE_CONTINUOUS_BEFORE = -5 + const val VOICE_MESSAGE_CONTINUOUS_AFTER = 5 + const val VOICE_MESSAGE_PLAY_ADD_THRESHOLD = 0.1 + const val VOICE_MESSAGE_MARK_PLAYED_FACTOR = 20 + const val OUT_OF_OFFICE_ALPHA = 76 + const val ZERO_INDEX = 0 + const val ONE_INDEX = 1 + const val MAX_AMOUNT_MEDIA_FILE_PICKER = 10 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt new file mode 100644 index 0000000..3225d13 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt @@ -0,0 +1,1126 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus juliuslinus1@gmail.com + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat + +import android.content.res.Resources +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Bundle +import android.os.CountDownTimer +import android.os.SystemClock +import android.text.Editable +import android.text.InputFilter +import android.text.TextUtils +import android.text.TextWatcher +import android.util.Log +import android.util.TypedValue +import android.view.Gravity +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.view.animation.Animation.AnimationListener +import android.view.animation.LinearInterpolator +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.PopupMenu +import android.widget.RelativeLayout +import android.widget.SeekBar +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.view.ContextThemeWrapper +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toDrawable +import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged +import androidx.emoji2.widget.EmojiTextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import autodagger.AutoInjector +import coil.Coil.imageLoader +import coil.load +import coil.request.ImageRequest +import coil.target.Target +import coil.transform.CircleCropTransformation +import com.google.android.flexbox.FlexboxLayout +import com.google.android.material.button.MaterialButton +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.callbacks.MentionAutocompleteCallback +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel +import com.nextcloud.talk.data.network.NetworkMonitor +import com.nextcloud.talk.databinding.FragmentMessageInputBinding +import com.nextcloud.talk.jobs.UploadAndShareFilesWorker +import com.nextcloud.talk.models.json.chat.ChatUtils +import com.nextcloud.talk.models.json.mention.Mention +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage +import com.nextcloud.talk.presenters.MentionAutocompletePresenter +import com.nextcloud.talk.ui.MicInputCloud +import com.nextcloud.talk.ui.dialog.AttachmentDialog +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.CharPolicy +import com.nextcloud.talk.utils.EmojiTextInputEditText +import com.nextcloud.talk.utils.ImageEmojiEditText +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.text.Spans +import com.otaliastudios.autocomplete.Autocomplete +import com.vanniktech.emoji.EmojiPopup +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.Objects +import javax.inject.Inject + +@Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "LongMethod") +@AutoInjector(NextcloudTalkApplication::class) +class MessageInputFragment : Fragment() { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var networkMonitor: NetworkMonitor + + @Inject + lateinit var messageUtils: MessageUtils + + lateinit var binding: FragmentMessageInputBinding + private lateinit var conversationInternalId: String + private var typedWhileTypingTimerIsRunning: Boolean = false + private var typingTimer: CountDownTimer? = null + private lateinit var chatActivity: ChatActivity + private var emojiPopup: EmojiPopup? = null + private var mentionAutocomplete: Autocomplete<*>? = null + private var xcounter = 0f + private var ycounter = 0f + private var collapsed = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + conversationInternalId = arguments?.getString(ChatActivity.CONVERSATION_INTERNAL_ID).orEmpty() + chatActivity = requireActivity() as ChatActivity + val sharedText = arguments?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty() + if (sharedText.isNotEmpty()) { + chatActivity.chatViewModel.messageDraft.messageText = sharedText + chatActivity.chatViewModel.saveMessageDraft() + } + if (conversationInternalId.isEmpty()) { + Log.e(TAG, "internalId for conversation passed to MessageInputFragment is empty") + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentMessageInputBinding.inflate(inflater) + themeMessageInputView() + initMessageInputView() + initSmileyKeyboardToggler() + setupMentionAutocomplete() + initVoiceRecordButton() + initThreadHandling() + restoreState() + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { + mentionAutocomplete?.dismissPopup() + } + clearEditUI() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initObservers() + + binding.fragmentCreateThreadView.createThreadView.findViewById( + R.id + .createThreadInput + ).doAfterTextChanged { text -> + val threadTitle = text.toString() + chatActivity.chatViewModel.messageDraft.threadTitle = threadTitle + } + } + + private fun initObservers() { + Log.d(TAG, "LifeCyclerOwner is: ${viewLifecycleOwner.lifecycle}") + chatActivity.messageInputViewModel.getReplyChatMessage.observe(viewLifecycleOwner) { message -> + message?.let { + chatActivity.chatViewModel.messageDraft.quotedMessageText = message.text + chatActivity.chatViewModel.messageDraft.quotedDisplayName = message.actorDisplayName + chatActivity.chatViewModel.messageDraft.quotedImageUrl = message.imageUrl + chatActivity.chatViewModel.messageDraft.quotedJsonId = message.jsonMessageId + replyToMessage( + message.text, + message.actorDisplayName, + message.imageUrl + ) + } ?: clearReplyUi() + } + + chatActivity.messageInputViewModel.getEditChatMessage.observe(viewLifecycleOwner) { message -> + message?.let { setEditUI(it as ChatMessage) } + } + + chatActivity.messageInputViewModel.createThreadViewState.observe(viewLifecycleOwner) { state -> + when (state) { + is MessageInputViewModel.CreateThreadStartState -> + binding.fragmentCreateThreadView.createThreadView.visibility = View.GONE + + is MessageInputViewModel.CreateThreadEditState -> { + binding.fragmentCreateThreadView.createThreadView.visibility = View.VISIBLE + binding.fragmentCreateThreadView.createThreadView + .findViewById(R.id.createThreadInput)?.setText( + chatActivity.chatViewModel.messageDraft.threadTitle + ) + } + + else -> {} + } + initVoiceRecordButton() + } + + chatActivity.chatViewModel.leaveRoomViewState.observe(viewLifecycleOwner) { state -> + when (state) { + is ChatViewModel.LeaveRoomSuccessState -> sendStopTypingMessage() + else -> {} + } + } + + viewLifecycleOwner.lifecycleScope.launch { + var wasOnline: Boolean + networkMonitor.isOnline + .onEach { isOnline -> + wasOnline = !binding.fragmentConnectionLost.isShown + val connectionGained = (!wasOnline && isOnline) + Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained") + if (connectionGained) { + chatActivity.messageInputViewModel.sendUnsentMessages( + chatActivity.conversationUser!!.getCredentials(), + ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser!!.baseUrl!!, + chatActivity.roomToken + ) + ) + } + handleUI(isOnline, connectionGained) + }.collect() + } + + chatActivity.messageInputViewModel.callStartedFlow.observe(viewLifecycleOwner) { + val (message, show) = it + if (show) { + binding.fragmentCallStarted.callAuthorChip.text = message.actorDisplayName + binding.fragmentCallStarted.callAuthorChipSecondary.text = message.actorDisplayName + val user = currentUserProvider.currentUser.blockingGet() + val url: String = if (message.actorType == "guests" || message.actorType == "guest") { + ApiUtils.getUrlForGuestAvatar(user!!.baseUrl!!, message.actorDisplayName, true) + } else { + ApiUtils.getUrlForAvatar(user!!.baseUrl!!, message.actorId, false) + } + + val imageRequest: ImageRequest = ImageRequest.Builder(requireContext()) + .data(url) + .crossfade(true) + .transformations(CircleCropTransformation()) + .target(object : Target { + override fun onStart(placeholder: Drawable?) { + // unused atm + } + + override fun onError(error: Drawable?) { + // unused atm + } + + override fun onSuccess(result: Drawable) { + binding.fragmentCallStarted.callAuthorChip.chipIcon = result + binding.fragmentCallStarted.callAuthorChipSecondary.chipIcon = result + } + }) + .build() + + imageLoader(requireContext()).enqueue(imageRequest) + binding.fragmentCallStarted.root.visibility = View.VISIBLE + } else { + binding.fragmentCallStarted.root.visibility = View.GONE + } + } + } + + private fun handleUI(isOnline: Boolean, connectionGained: Boolean) { + if (isOnline) { + if (connectionGained) { + val animation: Animation = AlphaAnimation(FULLY_OPAQUE, FULLY_TRANSPARENT) + animation.duration = CONNECTION_ESTABLISHED_ANIM_DURATION + animation.interpolator = LinearInterpolator() + binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityGreen)) + binding.fragmentConnectionLost.text = getString(R.string.connection_established) + binding.fragmentConnectionLost.startAnimation(animation) + binding.fragmentConnectionLost.animation.setAnimationListener(object : AnimationListener { + override fun onAnimationStart(animation: Animation?) { + // unused atm + } + + override fun onAnimationEnd(animation: Animation?) { + binding.fragmentConnectionLost.visibility = View.GONE + binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityRed)) + binding.fragmentConnectionLost.text = + getString(R.string.connection_lost_sent_messages_are_queued) + } + + override fun onAnimationRepeat(animation: Animation?) { + // unused atm + } + }) + } + + binding.fragmentMessageInputView.attachmentButton.visibility = View.VISIBLE + binding.fragmentMessageInputView.recordAudioButton.visibility = + if (binding.fragmentMessageInputView.inputEditText.text.isEmpty()) View.VISIBLE else View.GONE + } else { + binding.fragmentMessageInputView.attachmentButton.visibility = View.INVISIBLE + binding.fragmentMessageInputView.recordAudioButton.visibility = View.INVISIBLE + binding.fragmentConnectionLost.clearAnimation() + binding.fragmentConnectionLost.visibility = View.GONE + binding.fragmentConnectionLost.setBackgroundColor(resources.getColor(R.color.hwSecurityRed)) + binding.fragmentConnectionLost.visibility = View.VISIBLE + } + } + + private fun restoreState() { + CoroutineScope(Dispatchers.IO).launch { + chatActivity.chatViewModel.updateMessageDraft() + + withContext(Dispatchers.Main) { + val draft = chatActivity.chatViewModel.messageDraft + binding.fragmentMessageInputView.messageInput.setText(draft.messageText) + binding.fragmentMessageInputView.messageInput.setSelection(draft.messageCursor) + + if (draft.threadTitle?.isNotEmpty() == true) { + chatActivity.messageInputViewModel.startThreadCreation() + } + + if (draft.messageText != "") { + binding.fragmentMessageInputView.messageInput.requestFocus() + } + + if (isInReplyState()) { + replyToMessage( + chatActivity.chatViewModel.messageDraft.quotedMessageText, + chatActivity.chatViewModel.messageDraft.quotedDisplayName, + chatActivity.chatViewModel.messageDraft.quotedImageUrl + ) + } + } + } + } + + private fun initMessageInputView() { + if (!chatActivity.active) return + + val filters = arrayOfNulls(1) + val lengthFilter = CapabilitiesUtil.getMessageMaxLength(chatActivity.spreedCapabilities) + + binding.fragmentEditView.editMessageView.visibility = View.GONE + binding.fragmentMessageInputView.setPadding(0, 0, 0, 0) + + filters[0] = InputFilter.LengthFilter(lengthFilter) + binding.fragmentMessageInputView.inputEditText?.filters = filters + + binding.fragmentMessageInputView.inputEditText?.addTextChangedListener(object : TextWatcher { + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // unused atm + } + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + updateOwnTypingStatus(s) + + if (s.length >= lengthFilter) { + binding.fragmentMessageInputView.inputEditText?.error = String.format( + Objects.requireNonNull(resources).getString(R.string.nc_limit_hit), + lengthFilter.toString() + ) + } else { + binding.fragmentMessageInputView.inputEditText?.error = null + } + + val editable = binding.fragmentMessageInputView.inputEditText?.editableText + + if (editable != null && binding.fragmentMessageInputView.inputEditText != null) { + val mentionSpans = editable.getSpans( + 0, + binding.fragmentMessageInputView.inputEditText!!.length(), + Spans.MentionChipSpan::class.java + ) + var mentionSpan: Spans.MentionChipSpan + for (i in mentionSpans.indices) { + mentionSpan = mentionSpans[i] + if (start >= editable.getSpanStart(mentionSpan) && + start < editable.getSpanEnd(mentionSpan) + ) { + if (editable.subSequence( + editable.getSpanStart(mentionSpan), + editable.getSpanEnd(mentionSpan) + ).toString().trim() != mentionSpan.label + ) { + editable.removeSpan(mentionSpan) + } + } + } + } + } + + override fun afterTextChanged(s: Editable) { + val cursor = binding.fragmentMessageInputView.messageInput.selectionStart + val text = binding.fragmentMessageInputView.messageInput.text.toString() + chatActivity.chatViewModel.messageDraft.messageCursor = cursor + chatActivity.chatViewModel.messageDraft.messageText = text + handleButtonsVisibility() + } + }) + + // Image keyboard support + // See: https://developer.android.com/guide/topics/text/image-keyboard + + (binding.fragmentMessageInputView.inputEditText as ImageEmojiEditText).onCommitContentListener = { + chatActivity.chatViewModel.uploadFile( + fileUri = it.toString(), + isVoiceMessage = false, + caption = "", + roomToken = chatActivity.roomToken, + replyToMessageId = chatActivity.getReplyToMessageId(), + displayName = chatActivity.currentConversation?.displayName!! + ) + } + + binding.fragmentMessageInputView.setAttachmentsListener { + AttachmentDialog(requireActivity(), requireActivity() as ChatActivity).show() + } + + binding.fragmentMessageInputView.attachmentButton.setOnLongClickListener { + chatActivity.showGalleryPicker() + true + } + + binding.fragmentMessageInputView.button?.setOnClickListener { + submitMessage(false) + } + + binding.fragmentMessageInputView.editMessageButton.setOnClickListener { + val editable = binding.fragmentMessageInputView.inputEditText!!.editableText + replaceMentionChipSpans(editable) + val inputEditText = editable.toString() + + val message = chatActivity.messageInputViewModel.getEditChatMessage.value as ChatMessage + if (message.message!!.trim() != inputEditText.trim()) { + if (message.messageParameters != null) { + val editedMessage = messageUtils.processEditMessageParameters( + message.messageParameters!!, + message, + inputEditText + ) + editMessageAPI(message, editedMessage.toString()) + } else { + editMessageAPI(message, inputEditText.toString()) + } + } + clearEditUI() + } + binding.fragmentEditView.clearEdit.setOnClickListener { + clearEditUI() + } + binding.fragmentCreateThreadView.abortCreateThread.setOnClickListener { + cancelCreateThread() + } + + if (CapabilitiesUtil.hasSpreedFeatureCapability(chatActivity.spreedCapabilities, SpreedFeatures.SILENT_SEND)) { + binding.fragmentMessageInputView.button?.setOnLongClickListener { + showSendButtonMenu() + true + } + } + + binding.fragmentMessageInputView.button?.contentDescription = + resources.getString(R.string.nc_description_send_message_button) + + binding.fragmentCallStarted.joinAudioCall.setOnClickListener { + chatActivity.joinAudioCall() + } + + binding.fragmentCallStarted.joinVideoCall.setOnClickListener { + chatActivity.joinVideoCall() + } + + binding.fragmentCallStarted.callStartedCloseBtn.setOnClickListener { + collapsed = !collapsed + binding.fragmentCallStarted.callAuthorLayout.visibility = if (collapsed) View.GONE else View.VISIBLE + binding.fragmentCallStarted.callBtnLayout.visibility = if (collapsed) View.GONE else View.VISIBLE + binding.fragmentCallStarted.callAuthorChipSecondary.visibility = if (collapsed) View.VISIBLE else View.GONE + binding.fragmentCallStarted.callStartedSecondaryText.visibility = if (collapsed) View.VISIBLE else View.GONE + setDropDown(collapsed) + } + + binding.fragmentMessageInputView.findViewById(R.id.cancelReplyButton)?.setOnClickListener { + cancelReply() + } + } + + private fun setDropDown(collapsed: Boolean) { + val drawable = if (collapsed) { + AppCompatResources.getDrawable(requireContext(), R.drawable.ic_keyboard_arrow_up) + } else { + AppCompatResources.getDrawable(requireContext(), R.drawable.ic_keyboard_arrow_down) + } + + binding.fragmentCallStarted.callStartedCloseBtn.setImageDrawable(drawable) + } + + @Suppress("ClickableViewAccessibility", "CyclomaticComplexMethod", "LongMethod") + private fun initVoiceRecordButton() { + handleButtonsVisibility() + + var prevDx = 0f + var voiceRecordStartTime = 0L + var voiceRecordEndTime: Long + binding.fragmentMessageInputView.recordAudioButton.setOnTouchListener { v, event -> + v?.performClick() + when (event?.action) { + MotionEvent.ACTION_DOWN -> { + if (!chatActivity.isRecordAudioPermissionGranted()) { + chatActivity.requestRecordAudioPermissions() + return@setOnTouchListener true + } + if (!chatActivity.permissionUtil.isFilesPermissionGranted()) { + UploadAndShareFilesWorker.requestStoragePermission(chatActivity) + return@setOnTouchListener true + } + + val base = SystemClock.elapsedRealtime() + voiceRecordStartTime = System.currentTimeMillis() + binding.fragmentMessageInputView.audioRecordDuration.base = base + chatActivity.messageInputViewModel.setRecordingTime(base) + binding.fragmentMessageInputView.audioRecordDuration.start() + chatActivity.chatViewModel.startAudioRecording(requireContext(), chatActivity.currentConversation!!) + showRecordAudioUi(true) + } + + MotionEvent.ACTION_CANCEL -> { + Log.d(TAG, "ACTION_CANCEL") + if (chatActivity.chatViewModel.getVoiceRecordingInProgress.value == false || + !chatActivity.isRecordAudioPermissionGranted() + ) { + return@setOnTouchListener true + } + + showRecordAudioUi(false) + if (chatActivity.chatViewModel.getVoiceRecordingLocked.value != true) { // can also be null + chatActivity.chatViewModel.stopAndDiscardAudioRecording() + } + } + + MotionEvent.ACTION_UP -> { + Log.d(TAG, "ACTION_UP") + if (chatActivity.chatViewModel.getVoiceRecordingInProgress.value == false || + chatActivity.chatViewModel.getVoiceRecordingLocked.value == true || + !chatActivity.isRecordAudioPermissionGranted() + ) { + return@setOnTouchListener false + } + showRecordAudioUi(false) + + voiceRecordEndTime = System.currentTimeMillis() + val voiceRecordDuration = voiceRecordEndTime - voiceRecordStartTime + if (voiceRecordDuration < MINIMUM_VOICE_RECORD_DURATION) { + Snackbar.make( + binding.root, + requireContext().getString(R.string.nc_voice_message_hold_to_record_info), + Snackbar.LENGTH_SHORT + ).show() + chatActivity.chatViewModel.stopAndDiscardAudioRecording() + return@setOnTouchListener false + } else { + chatActivity.chatViewModel.stopAndSendAudioRecording( + roomToken = chatActivity.roomToken, + replyToMessageId = chatActivity.getReplyToMessageId(), + displayName = chatActivity.currentConversation!!.displayName + ) + } + resetSlider() + } + + MotionEvent.ACTION_MOVE -> { + if (chatActivity.chatViewModel.getVoiceRecordingInProgress.value == false || + !chatActivity.isRecordAudioPermissionGranted() + ) { + return@setOnTouchListener false + } + + if (event.x < VOICE_RECORD_CANCEL_SLIDER_X) { + chatActivity.chatViewModel.stopAndDiscardAudioRecording() + showRecordAudioUi(false) + resetSlider() + return@setOnTouchListener true + } + if (event.x < 0f) { + val dX = event.x + if (dX < prevDx) { // left + binding.fragmentMessageInputView.slideToCancelDescription.x -= INCREMENT + xcounter += INCREMENT + } else { // right + binding.fragmentMessageInputView.slideToCancelDescription.x += INCREMENT + xcounter -= INCREMENT + } + + prevDx = dX + } + + if (event.y < 0f) { + chatActivity.chatViewModel.postToRecordTouchObserver(INCREMENT) + ycounter += INCREMENT + } + + if (ycounter >= VOICE_RECORD_LOCK_THRESHOLD) { + resetSlider() + binding.fragmentMessageInputView.recordAudioButton.isEnabled = false + chatActivity.chatViewModel.setVoiceRecordingLocked(true) + binding.fragmentMessageInputView.recordAudioButton.isEnabled = true + } + } + } + v?.onTouchEvent(event) != false + } + } + + private fun initThreadHandling() { + binding.fragmentMessageInputView.submitThreadButton.setOnClickListener { + submitMessage(false) + } + + binding.fragmentCreateThreadView.createThreadInput.doAfterTextChanged { + handleButtonsVisibility() + } + } + + private fun handleButtonsVisibility() { + fun View.setVisible(isVisible: Boolean) { + visibility = if (isVisible) View.VISIBLE else View.GONE + } + + val isEditModeActive = binding.fragmentEditView.editMessageView.isVisible + val isThreadCreateModeActive = binding.fragmentCreateThreadView.createThreadView.isVisible + val inputContainsText = binding.fragmentMessageInputView.messageInput.text.isNotEmpty() + val threadTitleContainsText = binding.fragmentCreateThreadView.createThreadInput.text?.isNotEmpty() ?: false + + binding.fragmentMessageInputView.apply { + when { + isEditModeActive -> { + messageSendButton.setVisible(false) + recordAudioButton.setVisible(false) + submitThreadButton.setVisible(false) + attachmentButton.setVisible(true) + } + + isThreadCreateModeActive -> { + messageSendButton.setVisible(false) + recordAudioButton.setVisible(false) + attachmentButton.setVisible(false) + submitThreadButton.setVisible(true) + if (inputContainsText && threadTitleContainsText) { + submitThreadButton.isEnabled = true + submitThreadButton.alpha = FULLY_OPAQUE + } else { + submitThreadButton.isEnabled = false + submitThreadButton.alpha = OPACITY_DISABLED + } + } + + inputContainsText -> { + recordAudioButton.setVisible(false) + submitThreadButton.setVisible(false) + messageSendButton.setVisible(true) + attachmentButton.setVisible(true) + } + + else -> { + messageSendButton.setVisible(false) + submitThreadButton.setVisible(false) + recordAudioButton.setVisible(true) + attachmentButton.setVisible(true) + } + } + } + } + + private fun resetSlider() { + binding.fragmentMessageInputView.audioRecordDuration.stop() + binding.fragmentMessageInputView.audioRecordDuration.clearAnimation() + binding.fragmentMessageInputView.slideToCancelDescription.x += xcounter + chatActivity.chatViewModel.postToRecordTouchObserver(-ycounter) + xcounter = 0f + ycounter = 0f + } + + private fun setupMentionAutocomplete() { + val elevation = MENTION_AUTO_COMPLETE_ELEVATION + resources.let { + val backgroundDrawable = it.getColor(R.color.bg_default, null).toDrawable() + val presenter = MentionAutocompletePresenter( + requireContext(), + chatActivity.roomToken, + chatActivity.chatApiVersion + ) + val callback = MentionAutocompleteCallback( + requireContext(), + chatActivity.conversationUser!!, + binding.fragmentMessageInputView.inputEditText, + viewThemeUtils + ) + + if (mentionAutocomplete == null && binding.fragmentMessageInputView.inputEditText != null) { + mentionAutocomplete = + Autocomplete.on(binding.fragmentMessageInputView.inputEditText) + .with(elevation) + .with(backgroundDrawable) + .with(CharPolicy('@')) + .with(presenter) + .with(callback) + .build() + } + } + } + + private fun showRecordAudioUi(show: Boolean) { + if (show) { + val animation: Animation = AlphaAnimation(FULLY_OPAQUE, FULLY_TRANSPARENT) + animation.duration = ANIMATION_DURATION + animation.interpolator = LinearInterpolator() + animation.repeatCount = Animation.INFINITE + animation.repeatMode = Animation.REVERSE + binding.fragmentMessageInputView.microphoneEnabledInfo.startAnimation(animation) + + binding.fragmentMessageInputView.microphoneEnabledInfo.visibility = View.VISIBLE + binding.fragmentMessageInputView.microphoneEnabledInfoBackground.visibility = View.VISIBLE + binding.fragmentMessageInputView.audioRecordDuration.visibility = View.VISIBLE + binding.fragmentMessageInputView.slideToCancelDescription.visibility = View.VISIBLE + binding.fragmentMessageInputView.attachmentButton.visibility = View.GONE + binding.fragmentMessageInputView.smileyButton.visibility = View.GONE + binding.fragmentMessageInputView.messageInput.visibility = View.GONE + binding.fragmentMessageInputView.messageInput.hint = "" + } else { + binding.fragmentMessageInputView.microphoneEnabledInfo.clearAnimation() + + binding.fragmentMessageInputView.microphoneEnabledInfo.visibility = View.GONE + binding.fragmentMessageInputView.microphoneEnabledInfoBackground.visibility = View.GONE + binding.fragmentMessageInputView.audioRecordDuration.visibility = View.GONE + binding.fragmentMessageInputView.slideToCancelDescription.visibility = View.GONE + binding.fragmentMessageInputView.attachmentButton.visibility = View.VISIBLE + binding.fragmentMessageInputView.smileyButton.visibility = View.VISIBLE + binding.fragmentMessageInputView.messageInput.visibility = View.VISIBLE + binding.fragmentMessageInputView.messageInput.hint = + requireContext().resources?.getString(R.string.nc_hint_enter_a_message) + } + } + + private fun initSmileyKeyboardToggler() { + val smileyButton = binding.fragmentMessageInputView.findViewById(R.id.smileyButton) + + emojiPopup = binding.fragmentMessageInputView.inputEditText?.let { + EmojiPopup( + rootView = binding.root, + editText = it, + onEmojiPopupShownListener = { + smileyButton?.setImageDrawable( + ContextCompat.getDrawable(requireContext(), R.drawable.ic_baseline_keyboard_24) + ) + }, + onEmojiPopupDismissListener = { + smileyButton?.setImageDrawable( + ContextCompat.getDrawable(requireContext(), R.drawable.ic_insert_emoticon_black_24dp) + ) + }, + onEmojiClickListener = { + binding.fragmentMessageInputView.inputEditText?.editableText?.append(" ") + } + ) + } + + smileyButton?.setOnClickListener { + emojiPopup?.toggle() + } + } + + private fun replyToMessage(quotedMessageText: String?, quotedActorDisplayName: String?, quotedImageUrl: String?) { + Log.d(TAG, "Reply") + val view = binding.fragmentMessageInputView + view.findViewById(R.id.cancelReplyButton)?.visibility = + View.VISIBLE + + val quotedMessage = view.findViewById(R.id.quotedMessage) + + quotedMessage?.maxLines = 2 + quotedMessage?.ellipsize = TextUtils.TruncateAt.END + quotedMessage?.text = quotedMessageText + view.findViewById(R.id.quotedMessageAuthor)?.text = + quotedActorDisplayName ?: requireContext().getText(R.string.nc_nick_guest) + + chatActivity.conversationUser?.let { + val quotedMessageImage = view.findViewById(R.id.quotedMessageImage) + quotedImageUrl?.let { previewImageUrl -> + quotedMessageImage?.visibility = View.VISIBLE + + val px = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + QUOTED_MESSAGE_IMAGE_MAX_HEIGHT, + resources.displayMetrics + ) + + quotedMessageImage?.maxHeight = px.toInt() + val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams + layoutParams.flexGrow = 0f + quotedMessageImage.layoutParams = layoutParams + quotedMessageImage.load(previewImageUrl) { + addHeader("Authorization", chatActivity.credentials!!) + } + } ?: run { + view.findViewById(R.id.quotedMessageImage)?.visibility = View.GONE + } + } + + val quotedChatMessageView = view.findViewById(R.id.quotedChatMessageView) + quotedChatMessageView?.visibility = View.VISIBLE + } + + fun updateOwnTypingStatus(typedText: CharSequence) { + fun sendStartTypingSignalingMessage() { + val concurrentSafeHashMap = chatActivity.webSocketInstance?.getUserMap() + if (concurrentSafeHashMap != null) { + for ((sessionId, _) in concurrentSafeHashMap) { + val ncSignalingMessage = NCSignalingMessage() + ncSignalingMessage.to = sessionId + ncSignalingMessage.type = TYPING_STARTED_SIGNALING_MESSAGE_TYPE + chatActivity.signalingMessageSender!!.send(ncSignalingMessage) + } + } + } + + if (isTypingStatusEnabled()) { + if (typedText.isEmpty()) { + sendStopTypingMessage() + } else if (typingTimer == null) { + sendStartTypingSignalingMessage() + + typingTimer = object : CountDownTimer( + TYPING_DURATION_TO_SEND_NEXT_TYPING_MESSAGE, + TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE + ) { + override fun onTick(millisUntilFinished: Long) { + // unused + } + + override fun onFinish() { + if (typedWhileTypingTimerIsRunning) { + sendStartTypingSignalingMessage() + cancel() + start() + typedWhileTypingTimerIsRunning = false + } else { + sendStopTypingMessage() + } + } + }.start() + } else { + typedWhileTypingTimerIsRunning = true + } + } + } + + private fun sendStopTypingMessage() { + if (isTypingStatusEnabled()) { + typingTimer = null + typedWhileTypingTimerIsRunning = false + + val concurrentSafeHashMap = chatActivity.webSocketInstance?.getUserMap() + if (concurrentSafeHashMap != null) { + for ((sessionId, _) in concurrentSafeHashMap) { + val ncSignalingMessage = NCSignalingMessage() + ncSignalingMessage.to = sessionId + ncSignalingMessage.type = TYPING_STOPPED_SIGNALING_MESSAGE_TYPE + chatActivity.signalingMessageSender?.send(ncSignalingMessage) + } + } + } + } + + private fun isTypingStatusEnabled(): Boolean = + !CapabilitiesUtil.isTypingStatusPrivate(chatActivity.conversationUser!!) + + private fun submitMessage(sendWithoutNotification: Boolean) { + if (binding.fragmentMessageInputView.inputEditText != null) { + val editable = binding.fragmentMessageInputView.inputEditText!!.editableText + replaceMentionChipSpans(editable) + binding.fragmentMessageInputView.inputEditText?.setText("") + sendStopTypingMessage() + sendMessage( + editable.toString(), + sendWithoutNotification + ) + cancelReply() + cancelCreateThread() + } + } + + private fun sendMessage(message: String, sendWithoutNotification: Boolean) { + chatActivity.messageInputViewModel.sendChatMessage( + credentials = chatActivity.conversationUser!!.getCredentials(), + url = ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser!!.baseUrl!!, + chatActivity.roomToken + ), + message = message, + displayName = chatActivity.conversationUser!!.displayName ?: "", + replyTo = chatActivity.getReplyToMessageId(), + sendWithoutNotification = sendWithoutNotification, + threadTitle = chatActivity.chatViewModel.messageDraft.threadTitle + ) + } + + private fun replaceMentionChipSpans(editable: Editable) { + val mentionSpans = editable.getSpans( + 0, + editable.length, + Spans.MentionChipSpan::class.java + ) + for (mentionSpan in mentionSpans) { + var mentionId = mentionSpan.id + val shouldQuote = mentionId.contains(" ") || + mentionId.contains("@") || + mentionId.startsWith("guest/") || + mentionId.startsWith("group/") || + mentionId.startsWith("email/") || + mentionId.startsWith("team/") + if (shouldQuote) { + mentionId = "\"$mentionId\"" + } + editable.replace( + editable.getSpanStart(mentionSpan), + editable.getSpanEnd(mentionSpan), + "@$mentionId" + ) + } + } + + private fun showSendButtonMenu() { + val popupMenu = PopupMenu( + ContextThemeWrapper(requireContext(), R.style.ChatSendButtonMenu), + binding.fragmentMessageInputView.button, + Gravity.END + ) + popupMenu.inflate(R.menu.chat_send_menu) + + popupMenu.setOnMenuItemClickListener { item: MenuItem -> + when (item.itemId) { + R.id.send_without_notification -> submitMessage(true) + } + true + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + popupMenu.setForceShowIcon(true) + } + popupMenu.show() + } + + private fun editMessageAPI(message: ChatMessage, editedMessageText: String) { + // FIXME Fix API checking with guests? + val apiVersion: Int = ApiUtils.getChatApiVersion(chatActivity.spreedCapabilities, intArrayOf(1)) + + if (message.isTemporary) { + chatActivity.messageInputViewModel.editTempChatMessage( + message, + editedMessageText + ) + } else { + chatActivity.messageInputViewModel.editChatMessage( + chatActivity.credentials!!, + ApiUtils.getUrlForChatMessage( + apiVersion, + chatActivity.conversationUser!!.baseUrl!!, + chatActivity.roomToken, + message.id + ), + editedMessageText + ) + } + } + + private fun setEditUI(message: ChatMessage) { + val editedMessage = ChatUtils.getParsedMessage(message.message, message.messageParameters) + binding.fragmentEditView.editMessage.text = editedMessage + binding.fragmentMessageInputView.inputEditText.setText(editedMessage) + if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { + mentionAutocomplete?.dismissPopup() + } + val end = binding.fragmentMessageInputView.inputEditText.text.length + binding.fragmentMessageInputView.inputEditText.setSelection(end) + binding.fragmentMessageInputView.messageSendButton.visibility = View.GONE + binding.fragmentMessageInputView.recordAudioButton.visibility = View.GONE + binding.fragmentMessageInputView.submitThreadButton.visibility = View.GONE + binding.fragmentMessageInputView.editMessageButton.visibility = View.VISIBLE + binding.fragmentEditView.editMessageView.visibility = View.VISIBLE + binding.fragmentMessageInputView.attachmentButton.visibility = View.GONE + } + + private fun clearEditUI() { + binding.fragmentMessageInputView.editMessageButton.visibility = View.GONE + binding.fragmentMessageInputView.inputEditText.setText("") + binding.fragmentEditView.editMessageView.visibility = View.GONE + binding.fragmentMessageInputView.attachmentButton.visibility = View.VISIBLE + chatActivity.messageInputViewModel.edit(null) + handleButtonsVisibility() + } + + private fun themeMessageInputView() { + binding.fragmentMessageInputView.button?.let { viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) } + + binding.fragmentMessageInputView.findViewById(R.id.cancelReplyButton)?.let { + viewThemeUtils.platform + .themeImageButton(it) + } + + binding.fragmentMessageInputView.findViewById(R.id.playPauseBtn)?.let { + viewThemeUtils.material.colorMaterialButtonText(it) + } + + binding.fragmentMessageInputView.findViewById(R.id.seekbar)?.let { + viewThemeUtils.platform.themeHorizontalSeekBar(it) + } + + binding.fragmentMessageInputView.findViewById(R.id.deleteVoiceRecording)?.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) + } + binding.fragmentMessageInputView.findViewById(R.id.sendVoiceRecording)?.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) + } + + binding.fragmentMessageInputView.findViewById(R.id.microphoneEnabledInfo)?.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) + } + + binding.fragmentMessageInputView.findViewById(R.id.voice_preview_container)?.let { + viewThemeUtils.talk.themeOutgoingMessageBubble(it, true, false) + } + + binding.fragmentMessageInputView.findViewById(R.id.micInputCloud)?.let { + viewThemeUtils.talk.themeMicInputCloud(it) + } + binding.fragmentMessageInputView.findViewById(R.id.editMessageButton)?.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) + } + binding.fragmentEditView.clearEdit.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) + } + binding.fragmentCreateThreadView.abortCreateThread.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) + } + + binding.fragmentCallStarted.callStartedBackground.apply { + viewThemeUtils.talk.themeOutgoingMessageBubble(this, grouped = true, false) + } + + binding.fragmentCallStarted.callAuthorChip.apply { + viewThemeUtils.material.colorChipBackground(this) + } + + binding.fragmentCallStarted.callAuthorChipSecondary.apply { + viewThemeUtils.material.colorChipBackground(this) + } + + binding.fragmentCallStarted.callStartedCloseBtn.apply { + viewThemeUtils.platform.colorImageView(this, ColorRole.PRIMARY) + } + + binding.fragmentMessageInputView.submitThreadButton.apply { + viewThemeUtils.platform.colorImageView(this, ColorRole.SECONDARY) + } + + binding.fragmentCreateThreadView.createThreadInput.apply { + viewThemeUtils.platform.colorEditText(this) + } + } + + private fun cancelCreateThread() { + chatActivity.cancelCreateThread() + chatActivity.messageInputViewModel.stopThreadCreation() + binding.fragmentCreateThreadView.createThreadView.visibility = View.GONE + } + + private fun cancelReply() { + chatActivity.cancelReply() + clearReplyUi() + } + + private fun clearReplyUi() { + val quote = binding.fragmentMessageInputView.findViewById(R.id.quotedChatMessageView) + quote.visibility = View.GONE + binding.fragmentMessageInputView.findViewById(R.id.attachmentButton)?.visibility = View.VISIBLE + } + + private fun isInReplyState(): Boolean { + val jsonId = chatActivity.chatViewModel.messageDraft.quotedJsonId + return jsonId != null + } + + companion object { + fun newInstance() = MessageInputFragment() + private val TAG: String = MessageInputFragment::class.java.simpleName + private const val TYPING_DURATION_TO_SEND_NEXT_TYPING_MESSAGE = 10000L + private const val TYPING_INTERVAL_TO_SEND_NEXT_TYPING_MESSAGE = 1000L + private const val TYPING_STARTED_SIGNALING_MESSAGE_TYPE = "startedTyping" + private const val TYPING_STOPPED_SIGNALING_MESSAGE_TYPE = "stoppedTyping" + private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f + private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f + private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000 + private const val ANIMATION_DURATION: Long = 750 + private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -150 + private const val VOICE_RECORD_LOCK_THRESHOLD: Float = 100f + private const val INCREMENT = 8f + private const val CURSOR_KEY = "_cursor" + private const val CONNECTION_ESTABLISHED_ANIM_DURATION: Long = 3000 + private const val FULLY_OPAQUE: Float = 1.0f + private const val FULLY_TRANSPARENT: Float = 0.0f + private const val OPACITY_DISABLED = 0.7f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt b/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt new file mode 100644 index 0000000..98f529f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt @@ -0,0 +1,228 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus juliuslinus1@gmail.com + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat + +import android.os.Bundle +import android.os.SystemClock +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import autodagger.AutoInjector +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager +import com.nextcloud.talk.databinding.FragmentMessageInputVoiceRecordingBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class MessageInputVoiceRecordingFragment : Fragment() { + companion object { + val TAG: String = MessageInputVoiceRecordingFragment::class.java.simpleName + private const val SEEK_LIMIT = 98 + + @JvmStatic + fun newInstance() = MessageInputVoiceRecordingFragment() + } + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + lateinit var binding: FragmentMessageInputVoiceRecordingBinding + private lateinit var chatActivity: ChatActivity + private var pause = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentMessageInputVoiceRecordingBinding.inflate(inflater) + chatActivity = (requireActivity() as ChatActivity) + themeVoiceRecordingView() + initVoiceRecordingView() + initObservers() + this.lifecycle.addObserver(chatActivity.messageInputViewModel) + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + chatActivity.messageInputViewModel.stopMediaPlayer() // if it wasn't stopped already + this.lifecycle.removeObserver(chatActivity.messageInputViewModel) + } + + private fun initObservers() { + chatActivity.messageInputViewModel.startMicInput(requireContext()) + chatActivity.messageInputViewModel.micInputAudioObserver.observe(viewLifecycleOwner) { + binding.micInputCloud.setRotationSpeed(it.first, it.second) + } + + lifecycleScope.launch { + chatActivity.messageInputViewModel.mediaPlayerSeekbarObserver.onEach { progress -> + if (progress >= SEEK_LIMIT) { + togglePausePlay() + binding.seekbar.progress = 0 + } else if (!pause && chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) { + binding.seekbar.progress = progress + } + }.collect() + } + + chatActivity.messageInputViewModel.getAudioFocusChange.observe(viewLifecycleOwner) { state -> + when (state) { + AudioFocusRequestManager.ManagerState.AUDIO_FOCUS_CHANGE_LOSS -> { + if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) { + chatActivity.messageInputViewModel.stopMediaPlayer() + } + } + AudioFocusRequestManager.ManagerState.AUDIO_FOCUS_CHANGE_LOSS_TRANSIENT -> { + if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) { + chatActivity.messageInputViewModel.pauseMediaPlayer() + } + } + AudioFocusRequestManager.ManagerState.BROADCAST_RECEIVED -> { + if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) { + chatActivity.messageInputViewModel.pauseMediaPlayer() + } + } + } + } + } + + private fun initVoiceRecordingView() { + binding.deleteVoiceRecording.setOnClickListener { + chatActivity.chatViewModel.stopAndDiscardAudioRecording() + clear() + } + + binding.sendVoiceRecording.setOnClickListener { + chatActivity.chatViewModel.stopAndSendAudioRecording( + roomToken = chatActivity.roomToken, + replyToMessageId = chatActivity.getReplyToMessageId(), + displayName = chatActivity.currentConversation!!.displayName + ) + clear() + } + + binding.micInputCloud.setOnClickListener { + togglePreviewVisibility() + } + + binding.playPauseBtn.setOnClickListener { + togglePausePlay() + } + + binding.audioRecordDuration.base = chatActivity.messageInputViewModel.getRecordingTime.value ?: 0L + binding.audioRecordDuration.start() + + binding.seekbar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { + override fun onProgressChanged(seekbar: SeekBar, progress: Int, fromUser: Boolean) { + if (fromUser) { + chatActivity.messageInputViewModel.seekMediaPlayerTo(progress) + } + } + + override fun onStartTrackingTouch(p0: SeekBar) { + pause = true + } + + override fun onStopTrackingTouch(p0: SeekBar) { + pause = false + } + }) + } + + private fun clear() { + chatActivity.chatViewModel.setVoiceRecordingLocked(false) + chatActivity.messageInputViewModel.stopMicInput() + chatActivity.chatViewModel.stopAudioRecording() + chatActivity.messageInputViewModel.stopMediaPlayer() + binding.audioRecordDuration.stop() + binding.audioRecordDuration.clearAnimation() + } + + private fun togglePreviewVisibility() { + val visibility = binding.voicePreviewContainer.visibility + binding.voicePreviewContainer.visibility = if (visibility == View.VISIBLE) { + chatActivity.messageInputViewModel.stopMediaPlayer() + binding.playPauseBtn.icon = ContextCompat.getDrawable( + requireContext(), + R.drawable.ic_baseline_play_arrow_voice_message_24 + ) + pause = true + chatActivity.messageInputViewModel.startMicInput(requireContext()) + chatActivity.chatViewModel.startAudioRecording(requireContext(), chatActivity.currentConversation!!) + binding.audioRecordDuration.visibility = View.VISIBLE + binding.audioRecordDuration.base = SystemClock.elapsedRealtime() + binding.audioRecordDuration.start() + View.GONE + } else { + pause = false + binding.seekbar.progress = 0 + chatActivity.messageInputViewModel.stopMicInput() + chatActivity.chatViewModel.stopAudioRecording() + binding.audioRecordDuration.visibility = View.GONE + binding.audioRecordDuration.stop() + View.VISIBLE + } + } + + private fun togglePausePlay() { + val path = chatActivity.chatViewModel.getCurrentVoiceRecordFile() + if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) { + binding.playPauseBtn.icon = ContextCompat.getDrawable( + requireContext(), + R.drawable.ic_baseline_play_arrow_voice_message_24 + ) + chatActivity.messageInputViewModel.stopMediaPlayer() + } else { + binding.playPauseBtn.icon = ContextCompat.getDrawable( + requireContext(), + R.drawable.ic_baseline_pause_voice_message_24 + ) + chatActivity.messageInputViewModel.startMediaPlayer(path) + } + } + + private fun themeVoiceRecordingView() { + binding.playPauseBtn.let { + viewThemeUtils.material.colorMaterialButtonText(it) + } + + binding.seekbar.let { + viewThemeUtils.platform.themeHorizontalSeekBar(it) + } + + binding.deleteVoiceRecording.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) + } + binding.sendVoiceRecording.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) + } + + binding.voicePreviewContainer.let { + viewThemeUtils.talk.themeOutgoingMessageBubble(it, true, false) + } + + binding.micInputCloud.let { + viewThemeUtils.talk.themeMicInputCloud(it) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/OverflowMenu.kt b/app/src/main/java/com/nextcloud/talk/chat/OverflowMenu.kt new file mode 100644 index 0000000..fc403f6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/OverflowMenu.kt @@ -0,0 +1,160 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat + +import android.view.View +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import com.nextcloud.talk.R + +data class MenuItemData(val title: String, val subtitle: String? = null, val icon: Int? = null, val onClick: () -> Unit) + +@Composable +fun OverflowMenu(anchor: View?, expanded: Boolean, items: List, onDismiss: () -> Unit) { + if (!expanded) return + + val rect = anchor?.boundsInWindow() + val xOffset = rect?.left ?: 0 + val yOffset = rect?.bottom ?: 0 + + Popup( + onDismissRequest = onDismiss, + offset = IntOffset(xOffset, yOffset) + ) { + Column( + modifier = Modifier + .width(IntrinsicSize.Max) + .background( + color = colorResource(id = R.color.bg_default), + shape = RoundedCornerShape(1.dp) + ) + .shadow( + elevation = 1.dp, + shape = RoundedCornerShape(1.dp), + clip = false + ) + ) { + items.forEach { item -> + DynamicMenuItem( + item.copy( + onClick = { + item.onClick() + onDismiss() + } + ) + ) + } + } + } +} + +@Composable +fun DynamicMenuItem(item: MenuItemData) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = item.onClick, + interactionSource = remember { MutableInteractionSource() } + ) + .padding(horizontal = 12.dp, vertical = 12.dp) + ) { + item.icon?.let { icon -> + Icon( + painter = painterResource(icon), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + Spacer(Modifier.width(8.dp)) + } + + Column { + Text(item.title, color = MaterialTheme.colorScheme.onSurface) + item.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +private fun View.boundsInWindow(): android.graphics.Rect { + val location = IntArray(2) + getLocationOnScreen(location) + return android.graphics.Rect( + location[0], + location[1], + location[0] + width, + location[1] + height + ) +} + +@Preview +@Composable +fun OverflowMenuPreview() { + val items = listOf( + MenuItemData( + title = "first item title", + subtitle = "first item subtitle", + icon = R.drawable.baseline_notifications_24, + onClick = {} + ), + MenuItemData( + title = "second item title", + subtitle = null, + icon = R.drawable.outline_notifications_active_24, + onClick = {} + ), + MenuItemData( + title = "third item title", + subtitle = null, + icon = R.drawable.baseline_notifications_24, + onClick = {} + ), + MenuItemData( + title = "fourth item title", + subtitle = null, + icon = R.drawable.baseline_notifications_24, + onClick = {} + ) + ) + + OverflowMenu( + anchor = null, + expanded = true, + items = items, + onDismiss = { } + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/TypingParticipant.kt b/app/src/main/java/com/nextcloud/talk/chat/TypingParticipant.kt new file mode 100644 index 0000000..85d7e54 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/TypingParticipant.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.chat + +import android.os.CountDownTimer + +class TypingParticipant(val userId: String, val name: String, val funToCallWhenTimeIsUp: (userId: String) -> Unit) { + var timer: CountDownTimer? = null + + init { + startTimer() + } + + private fun startTimer() { + timer = object : CountDownTimer( + TYPING_DURATION_TO_HIDE_TYPING_MESSAGE, + TYPING_DURATION_TO_HIDE_TYPING_MESSAGE + ) { + override fun onTick(millisUntilFinished: Long) { + // unused + } + + override fun onFinish() { + funToCallWhenTimeIsUp(userId) + } + }.start() + } + + fun restartTimer() { + timer?.cancel() + timer?.start() + } + + fun cancelTimer() { + timer?.cancel() + } + + companion object { + private const val TYPING_DURATION_TO_HIDE_TYPING_MESSAGE = 15000L + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt new file mode 100644 index 0000000..e8dadd3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/ChatMessageRepository.kt @@ -0,0 +1,119 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.data + +import android.os.Bundle +import com.nextcloud.talk.chat.data.io.LifecycleAwareManager +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow + +interface ChatMessageRepository : LifecycleAwareManager { + + /** + * Stream of a list of messages to be handled using the associated boolean + * false for past messages, true for future messages. + */ + val messageFlow: + Flow< + Triple< + Boolean, + Boolean, + List + > + > + + val updateMessageFlow: Flow + + val lastCommonReadFlow: Flow + + val lastReadMessageFlow: Flow + + /** + * Used for informing the user of the underlying processing behind offline support, [String] is the key + * which is handled in a switch statement in ChatActivity. + */ + val generalUIFlow: Flow + + val removeMessageFlow: Flow + + fun initData(credentials: String, urlForChatting: String, roomToken: String, threadId: Long?) + + fun updateConversation(conversationModel: ConversationModel) + + fun initScopeAndLoadInitialMessages(withNetworkParams: Bundle) + + /** + * Loads messages from local storage. If the messages are not found, then it + * synchronizes the database with the server, before retrying exactly once. Only + * emits to [messageFlow] if the message list is not empty. + * + * [withNetworkParams] credentials and url + */ + fun loadMoreMessages( + beforeMessageId: Long, + roomToken: String, + withMessageLimit: Int, + withNetworkParams: Bundle + ): Job + + /** + * Long polls the server for any updates to the chat, if found, it synchronizes + * the database with the server and emits the new messages to [messageFlow], + * else it simply retries after timeout. + */ + fun initMessagePolling(initialMessageId: Long): Job + + /** + * Gets a individual message. + */ + suspend fun getMessage(messageId: Long, bundle: Bundle): Flow + + suspend fun getNumberOfThreadReplies(threadId: Long): Int + + @Suppress("LongParameterList") + suspend fun sendChatMessage( + credentials: String, + url: String, + message: String, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String, + threadTitle: String? + ): Flow> + + @Suppress("LongParameterList") + suspend fun resendChatMessage( + credentials: String, + url: String, + message: String, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String + ): Flow> + + suspend fun addTemporaryMessage( + message: CharSequence, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String + ): Flow> + + suspend fun editChatMessage(credentials: String, url: String, text: String): Flow> + + suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow + + suspend fun sendUnsentChatMessages(credentials: String, url: String) + + suspend fun deleteTempMessage(chatMessage: ChatMessage) +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioFocusRequestManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioFocusRequestManager.kt new file mode 100644 index 0000000..94d9413 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioFocusRequestManager.kt @@ -0,0 +1,102 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.data.io + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.media.AudioFocusRequest +import android.media.AudioManager +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +/** + * Abstraction over the [AudioFocusManager](https://developer.android.com/reference/kotlin/android/media/AudioFocusRequest) + * class used to manage audio focus requests automatically + */ +class AudioFocusRequestManager(private val context: Context) { + companion object { + val TAG: String? = AudioFocusRequestManager::class.java.simpleName + } + + enum class ManagerState { + AUDIO_FOCUS_CHANGE_LOSS, + AUDIO_FOCUS_CHANGE_LOSS_TRANSIENT, + BROADCAST_RECEIVED + } + + private val _getManagerState: MutableLiveData = MutableLiveData() + val getManagerState: LiveData + get() = _getManagerState + + private var isPausedDueToBecomingNoisy = false + private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + private val duration = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + private val audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener = + AudioManager.OnAudioFocusChangeListener { flag -> + when (flag) { + AudioManager.AUDIOFOCUS_LOSS -> { + isPausedDueToBecomingNoisy = false + _getManagerState.value = ManagerState.AUDIO_FOCUS_CHANGE_LOSS + } + + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + isPausedDueToBecomingNoisy = false + _getManagerState.value = ManagerState.AUDIO_FOCUS_CHANGE_LOSS_TRANSIENT + } + } + } + private val noisyAudioStreamReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + isPausedDueToBecomingNoisy = true + _getManagerState.value = ManagerState.BROADCAST_RECEIVED + } + } + + private val focusRequest = AudioFocusRequest.Builder(duration) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .build() + + /** + * Requests the OS for audio focus, before executing the callback on success + */ + fun audioFocusRequest(shouldRequestFocus: Boolean, onGranted: () -> Unit) { + if (isPausedDueToBecomingNoisy) { + onGranted() + return + } + + val isGranted: Int = + if (shouldRequestFocus) { + audioManager.requestAudioFocus(focusRequest) + } else { + audioManager.abandonAudioFocusRequest(focusRequest) + } + + if (isGranted == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + onGranted() + handleBecomingNoisyBroadcast(shouldRequestFocus) + } + } + + private fun handleBecomingNoisyBroadcast(register: Boolean) { + try { + if (register) { + context.registerReceiver( + noisyAudioStreamReceiver, + IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY) + ) + } else { + context.unregisterReceiver(noisyAudioStreamReceiver) + } + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioRecorderManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioRecorderManager.kt new file mode 100644 index 0000000..2a91e52 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/AudioRecorderManager.kt @@ -0,0 +1,143 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.data.io + +import android.Manifest +import android.content.Context +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.core.content.PermissionChecker +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.nextcloud.talk.ui.MicInputCloud +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.math.abs +import kotlin.math.log10 + +/** + * Abstraction over the [AudioRecord](https://developer.android.com/reference/android/media/AudioRecord) class used + * to manage the AudioRecord instance and the asynchronous updating of the MicInputCloud. Allows access to the raw + * bytes recorded from hardware. + */ +class AudioRecorderManager : LifecycleAwareManager { + + companion object { + val TAG: String = AudioRecorderManager::class.java.simpleName + private const val SAMPLE_RATE = 8000 + private const val AUDIO_MAX = 40 + private const val AUDIO_MIN = 20 + private const val AUDIO_INTERVAL = 50L + } + private val _getAudioValues: MutableLiveData> = MutableLiveData() + val getAudioValues: LiveData> + get() = _getAudioValues + + private var scope = MainScope() + private var loop = false + private var audioRecorder: AudioRecord? = null + private val bufferSize = AudioRecord.getMinBufferSize( + SAMPLE_RATE, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + + /** + * Initializes and starts the AudioRecorder. Posts updates to the callback every 50 ms. + */ + fun start(context: Context) { + if (audioRecorder == null || audioRecorder!!.state == AudioRecord.STATE_UNINITIALIZED) { + initAudioRecorder(context) + } + Log.d(TAG, "AudioRecorder started") + audioRecorder!!.startRecording() + loop = true + scope = MainScope().apply { + launch { + Log.d(TAG, "MicInputObserver started") + micInputObserver() + } + } + } + + /** + * Stops and destroys the AudioRecorder. Updates cancelled. + */ + fun stop() { + if (audioRecorder == null || audioRecorder!!.state == AudioRecord.STATE_UNINITIALIZED) { + Log.e(TAG, "Stopped AudioRecord on invalid state ") + return + } + Log.d(TAG, "AudioRecorder stopped") + loop = false + audioRecorder!!.stop() + audioRecorder!!.release() + audioRecorder = null + } + + private suspend fun micInputObserver() { + withContext(Dispatchers.IO) { + while (true) { + if (!loop) { + return@withContext + } + val byteArr = ByteArray(bufferSize / 2) + audioRecorder!!.read(byteArr, 0, byteArr.size) + val x = abs(byteArr[0].toFloat()) + val logX = log10(x) + if (x > AUDIO_MAX) { + _getAudioValues.postValue(Pair(logX, MicInputCloud.MAXIMUM_RADIUS)) + } else if (x > AUDIO_MIN) { + _getAudioValues.postValue(Pair(logX, MicInputCloud.EXTENDED_RADIUS)) + } else { + _getAudioValues.postValue(Pair(1f, MicInputCloud.DEFAULT_RADIUS)) + } + + delay(AUDIO_INTERVAL) + } + } + } + + private fun initAudioRecorder(context: Context) { + val permissionCheck = ContextCompat.checkSelfPermission( + context, + Manifest.permission.RECORD_AUDIO + ) + + if (permissionCheck == PermissionChecker.PERMISSION_GRANTED) { + Log.d(TAG, "AudioRecorder init") + audioRecorder = AudioRecord( + MediaRecorder.AudioSource.MIC, + SAMPLE_RATE, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT, + bufferSize + ) + } + } + + override fun handleOnPause() { + // unused atm + } + + override fun handleOnResume() { + // unused atm + } + + override fun handleOnStop() { + scope.cancel() + stop() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/LifecycleAwareManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/LifecycleAwareManager.kt new file mode 100644 index 0000000..7843605 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/LifecycleAwareManager.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.data.io + +/** + * Interface used by manager classes in the data layer. Enforces that every Manager handles the lifecycle events + * observed by the view model. + */ +interface LifecycleAwareManager { + /** + * See [onPause](https://developer.android.com/guide/components/activities/activity-lifecycle#onpause) + * for more details. + */ + fun handleOnPause() + + /** + * See [onResume](https://developer.android.com/guide/components/activities/activity-lifecycle#onresume) + * for more details. + */ + fun handleOnResume() + + /** + * See [onStop](https://developer.android.com/guide/components/activities/activity-lifecycle#onstop) + * for more details. + */ + fun handleOnStop() +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt new file mode 100644 index 0000000..d08bb2c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaPlayerManager.kt @@ -0,0 +1,350 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.data.io + +import android.media.MediaPlayer +import android.util.Log +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.ui.PlaybackSpeed +import com.nextcloud.talk.utils.preferences.AppPreferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileNotFoundException +import kotlin.math.ceil + +/** + * Abstraction over the [MediaPlayer](https://developer.android.com/reference/android/media/MediaPlayer) class used + * to manage the MediaPlayer instance. + */ +@Suppress("TooManyFunctions", "TooGenericExceptionCaught") +class MediaPlayerManager : LifecycleAwareManager { + companion object { + val TAG: String = MediaPlayerManager::class.java.simpleName + private const val SEEKBAR_UPDATE_DELAY = 150L + private const val ONE_SEC = 1000 + private const val DIVIDER = 100f + private const val IS_PLAYED_CUTOFF = 5 + + @JvmStatic + private val manager: MediaPlayerManager = MediaPlayerManager() + + fun sharedInstance(preferences: AppPreferences): MediaPlayerManager = + manager.apply { + appPreferences = preferences + } + } + + lateinit var appPreferences: AppPreferences + + enum class MediaPlayerManagerState { + DEFAULT, + SETUP, + STARTED, + STOPPED, + RESUMED, + PAUSED, + ERROR + } + + val backgroundPlayUIFlow: StateFlow + get() = _backgroundPlayUIFlow + private val _backgroundPlayUIFlow = MutableStateFlow(null) + + val managerState: Flow + get() = _managerState + private val _managerState = MutableStateFlow(MediaPlayerManagerState.DEFAULT) + + private val playQueue = mutableListOf>() + + val mediaPlayerSeekBarPositionMsg: Flow + get() = _mediaPlayerSeekBarPositionMsg + private val _mediaPlayerSeekBarPositionMsg: MutableSharedFlow = MutableSharedFlow() + + val mediaPlayerSeekBarPosition: Flow + get() = _mediaPlayerSeekBarPosition + private val _mediaPlayerSeekBarPosition: MutableSharedFlow = MutableSharedFlow() + + private var mediaPlayer: MediaPlayer? = null + private var loop = false + private var scope = MainScope() + private var currentCycledMessage: ChatMessage? = null + private var currentDataSource: String = "" + var mediaPlayerDuration: Int = 0 + var mediaPlayerPosition: Int = 0 + + /** + * Starts playing audio from the given path, initializes or resumes if the player is already created. + */ + fun start(path: String) { + if (mediaPlayer != null && mediaPlayer!!.isPlaying) { + stop() + } + + if (mediaPlayer == null || !scope.isActive) { + init(path) + } else { + _managerState.value = MediaPlayerManagerState.RESUMED + mediaPlayer!!.start() + loop = true + scope.launch { seekbarUpdateObserver() } + } + } + + /** + * Starting cycling through the playQueue, playing messages automatically unless stop() is called. + * + */ + fun startCycling() { + if (mediaPlayer != null && mediaPlayer!!.isPlaying) { + stop() + } + + val shouldReset = playQueue.first().first != currentDataSource + + if (mediaPlayer == null || !scope.isActive || shouldReset) { + initCycling() + } else { + _managerState.value = MediaPlayerManagerState.RESUMED + mediaPlayer!!.start() + loop = true + scope.launch { seekbarUpdateObserver() } + } + } + + /** + * Stop and destroys the player. + */ + fun stop() { + if (mediaPlayer != null) { + Log.d(TAG, "media player destroyed") + loop = false + scope.cancel() + mediaPlayer!!.stop() + mediaPlayer!!.release() + mediaPlayer = null + currentCycledMessage = null + _backgroundPlayUIFlow.tryEmit(null) + _managerState.value = MediaPlayerManagerState.STOPPED + } + } + + /** + * Pauses the player. + */ + fun pause(notifyUI: Boolean) { + if (mediaPlayer != null) { + Log.d(TAG, "media player paused") + _managerState.value = MediaPlayerManagerState.PAUSED + mediaPlayer!!.pause() + loop = false + if (notifyUI) { + _backgroundPlayUIFlow.tryEmit(null) + } + } + } + + /** + * Seeks the player to the given position, saves position for resynchronization. + */ + fun seekTo(progress: Int) { + if (mediaPlayer != null) { + val pos = mediaPlayer!!.duration * (progress / DIVIDER) + mediaPlayer!!.seekTo(pos.toInt()) + mediaPlayerPosition = pos.toInt() + } + } + + private suspend fun seekbarUpdateObserver() { + withContext(Dispatchers.IO) { + currentCycledMessage?.voiceMessageDuration = mediaPlayerDuration / ONE_SEC + currentCycledMessage?.resetVoiceMessage = false + while (true) { + if (!loop) { + // NOTE: ok so this doesn't stop the loop, but rather stop the update. Wasteful, but minimal + delay(SEEKBAR_UPDATE_DELAY) + continue + } + + mediaPlayer?.let { player -> + try { + if (!player.isPlaying) return@let + } catch (e: IllegalStateException) { + Log.e(TAG, "Seekbar updated during an improper state: $e") + return@let + } + + val pos = player.currentPosition + mediaPlayerPosition = pos + val progress = (pos.toFloat() / mediaPlayerDuration) * DIVIDER + val progressI = ceil(progress).toInt() + val seconds = (pos / ONE_SEC) + _mediaPlayerSeekBarPosition.emit(progressI) + currentCycledMessage?.let { msg -> + msg.isPlayingVoiceMessage = true + msg.voiceMessageSeekbarProgress = progressI + msg.voiceMessagePlayedSeconds = seconds + if (progressI >= IS_PLAYED_CUTOFF) msg.wasPlayedVoiceMessage = true + _mediaPlayerSeekBarPositionMsg.emit(msg) + } + } + + delay(SEEKBAR_UPDATE_DELAY) + } + } + } + + /** + * Adds a audio file to the play queue. for cycling through + * + * @throws FileNotFoundException if the file is not downloaded to cache first + */ + fun addToPlayList(path: String, chatMessage: ChatMessage) { + val file = File(path) + if (!file.exists()) { + throw FileNotFoundException("Cannot add to playlist without downloading to cache first for path\n$path") + } + + for (pair in playQueue) { + if (pair.first == path) return + } + + playQueue.add(Pair(path, chatMessage)) + } + + fun clearPlayList() { + playQueue.clear() + } + + /** + * Sets the player speed. + */ + fun setPlayBackSpeed(speed: PlaybackSpeed) { + if (mediaPlayer != null && mediaPlayer!!.isPlaying) { + mediaPlayer!!.playbackParams.let { params -> + params.setSpeed(speed.value) + mediaPlayer!!.playbackParams = params + } + } + } + + private fun init(path: String) { + try { + mediaPlayer = MediaPlayer().apply { + _managerState.value = MediaPlayerManagerState.SETUP + setDataSource(path) + currentDataSource = path + prepareAsync() + setOnPreparedListener { + onPrepare() + } + } + } catch (e: Exception) { + Log.e(ChatActivity.TAG, "failed to initialize mediaPlayer", e) + _managerState.value = MediaPlayerManagerState.ERROR + } + } + + private fun initCycling() { + try { + mediaPlayer = MediaPlayer().apply { + _managerState.value = MediaPlayerManagerState.SETUP + val pair = playQueue.iterator().next() + setDataSource(pair.first) + currentDataSource = pair.first + currentCycledMessage = pair.second + playQueue.removeAt(0) + prepareAsync() + setOnPreparedListener { + onPrepare() + } + + setOnCompletionListener { + if (playQueue.iterator().hasNext() && playQueue.first().first != currentDataSource) { + _managerState.value = MediaPlayerManagerState.SETUP + val nextPair = playQueue.iterator().next() + playQueue.removeAt(0) + mediaPlayer?.reset() + mediaPlayer?.setDataSource(nextPair.first) + currentCycledMessage = nextPair.second + prepare() + } else { + mediaPlayer?.release() + mediaPlayer = null + _backgroundPlayUIFlow.tryEmit(null) + currentCycledMessage?.let { + it.resetVoiceMessage = true + it.isPlayingVoiceMessage = false + } + runBlocking { + _mediaPlayerSeekBarPositionMsg.emit(currentCycledMessage!!) + } + currentCycledMessage = null + loop = false + _managerState.value = MediaPlayerManagerState.STOPPED + } + } + } + } catch (e: Exception) { + Log.e(ChatActivity.TAG, "failed to initialize mediaPlayer", e) + _managerState.value = MediaPlayerManagerState.ERROR + } + } + + private fun MediaPlayer.onPrepare() { + mediaPlayerDuration = this.duration + + val playBackSpeed = if (currentCycledMessage?.actorId == null) { + PlaybackSpeed.NORMAL.value + } else { + appPreferences.getPreferredPlayback(currentCycledMessage?.actorId).value + } + mediaPlayer!!.playbackParams.setSpeed(playBackSpeed) + + start() + _managerState.value = MediaPlayerManagerState.STARTED + currentCycledMessage?.let { + it.isPlayingVoiceMessage = true + _backgroundPlayUIFlow.tryEmit(it) + } + loop = true + scope = MainScope() + scope.launch { seekbarUpdateObserver() } + } + + override fun handleOnPause() { + // unused atm + } + + override fun handleOnResume() { + if (mediaPlayer != null && mediaPlayer!!.isPlaying) { + loop = true + } + } + + override fun handleOnStop() { + loop = false + if (mediaPlayer != null && currentCycledMessage != null && mediaPlayer!!.isPlaying) { + CoroutineScope(Dispatchers.Default).launch { + _backgroundPlayUIFlow.tryEmit(currentCycledMessage!!) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaRecorderManager.kt b/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaRecorderManager.kt new file mode 100644 index 0000000..a08ef50 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/io/MediaRecorderManager.kt @@ -0,0 +1,171 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.data.io + +import android.annotation.SuppressLint +import android.content.Context +import android.media.MediaRecorder +import android.util.Log +import com.nextcloud.talk.R +import com.nextcloud.talk.models.domain.ConversationModel +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date + +/** + * Abstraction over the [MediaRecorder](https://developer.android.com/reference/android/media/MediaRecorder) class + * used to manage the MediaRecorder instance and it's state changes. Google doesn't provide a way of accessing state + * directly, so this handles the changes without exposing the user to it. + */ +class MediaRecorderManager : LifecycleAwareManager { + + companion object { + val TAG: String = MediaRecorderManager::class.java.simpleName + private const val VOICE_MESSAGE_SAMPLING_RATE = 22050 + private const val VOICE_MESSAGE_ENCODING_BIT_RATE = 32000 + private const val VOICE_MESSAGE_CHANNELS = 1 + private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss" + private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3" + private const val VOICE_MESSAGE_PREFIX_MAX_LENGTH = 146 + } + + var currentVoiceRecordFile: String = "" + + enum class MediaRecorderState { + INITIAL, + INITIALIZED, + CONFIGURED, + PREPARED, + RECORDING, + RELEASED, + ERROR + } + private var _mediaRecorderState: MediaRecorderState = MediaRecorderState.INITIAL + val mediaRecorderState: MediaRecorderState + get() = _mediaRecorderState + private var recorder: MediaRecorder? = null + + /** + * Initializes and starts the MediaRecorder + */ + fun start(context: Context, currentConversation: ConversationModel) { + if (_mediaRecorderState == MediaRecorderState.ERROR || + _mediaRecorderState == MediaRecorderState.RELEASED + ) { + _mediaRecorderState = MediaRecorderState.INITIAL + } + + if (_mediaRecorderState == MediaRecorderState.INITIAL) { + setVoiceRecordFileName(context, currentConversation) + initAndStartRecorder() + } else { + Log.e(TAG, "Started MediaRecorder with invalid state ${_mediaRecorderState.name}") + } + } + + /** + * Stops and destroys the MediaRecorder + */ + fun stop() { + if (_mediaRecorderState != MediaRecorderState.RELEASED) { + stopAndDestroyRecorder() + } else { + Log.e(TAG, "Stopped MediaRecorder with invalid state ${_mediaRecorderState.name}") + } + } + + private fun initAndStartRecorder() { + recorder = MediaRecorder().apply { + setAudioSource(MediaRecorder.AudioSource.MIC) + _mediaRecorderState = MediaRecorderState.INITIALIZED + + setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) + _mediaRecorderState = MediaRecorderState.CONFIGURED + + setOutputFile(currentVoiceRecordFile) + setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + setAudioSamplingRate(VOICE_MESSAGE_SAMPLING_RATE) + setAudioEncodingBitRate(VOICE_MESSAGE_ENCODING_BIT_RATE) + setAudioChannels(VOICE_MESSAGE_CHANNELS) + + try { + prepare() + _mediaRecorderState = MediaRecorderState.PREPARED + } catch (e: IOException) { + _mediaRecorderState = MediaRecorderState.ERROR + Log.e(TAG, "prepare for audio recording failed") + } + + try { + start() + _mediaRecorderState = MediaRecorderState.RECORDING + Log.d(TAG, "recording started") + } catch (e: IllegalStateException) { + _mediaRecorderState = MediaRecorderState.ERROR + Log.e(TAG, "start for audio recording failed") + } + } + } + + @Suppress("TooGenericExceptionCaught") + private fun stopAndDestroyRecorder() { + recorder?.apply { + try { + if (_mediaRecorderState == MediaRecorderState.RECORDING) { + stop() + reset() + _mediaRecorderState = MediaRecorderState.INITIAL + Log.d(TAG, "stopped recorder") + } + release() + _mediaRecorderState = MediaRecorderState.RELEASED + } catch (e: Exception) { + when (e) { + is java.lang.IllegalStateException, + is java.lang.RuntimeException -> { + _mediaRecorderState = MediaRecorderState.ERROR + Log.e(TAG, "error while stopping recorder! with state $_mediaRecorderState $e") + } + } + } + } + recorder = null + } + + @SuppressLint("SimpleDateFormat") + private fun setVoiceRecordFileName(context: Context, currentConversation: ConversationModel) { + val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN) + val date: String = simpleDateFormat.format(Date()) + val regex = "[/\\\\:%]".toRegex() + val displayName = currentConversation.displayName.replace(regex, " ") + val validDisplayName = displayName.replace("\\s+".toRegex(), " ") + + var fileNameWithoutSuffix = String.format( + context.resources.getString(R.string.nc_voice_message_filename), + date, + validDisplayName + ) + if (fileNameWithoutSuffix.length > VOICE_MESSAGE_PREFIX_MAX_LENGTH) { + fileNameWithoutSuffix = fileNameWithoutSuffix.substring(0, VOICE_MESSAGE_PREFIX_MAX_LENGTH) + } + val fileName = fileNameWithoutSuffix + VOICE_MESSAGE_FILE_SUFFIX + currentVoiceRecordFile = "${context.cacheDir.absolutePath}/$fileName" + } + + override fun handleOnPause() { + // unused atm + } + + override fun handleOnResume() { + // unused atm + } + + override fun handleOnStop() { + stop() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt new file mode 100644 index 0000000..9fc6d36 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt @@ -0,0 +1,446 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022-2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.chat.data.model + +import android.text.TextUtils +import android.util.Log +import com.bluelinelabs.logansquare.annotation.JsonIgnore +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.database.model.SendStatus +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.stfalcon.chatkit.commons.models.IUser +import com.stfalcon.chatkit.commons.models.MessageContentType +import java.security.MessageDigest +import java.util.Date + +data class ChatMessage( + var isGrouped: Boolean = false, + + var isOneToOneConversation: Boolean = false, + + var isFormerOneToOneConversation: Boolean = false, + + var activeUser: User? = null, + + var selectedIndividualHashMap: Map? = null, + + var isDeleted: Boolean = false, + + var jsonMessageId: Int = 0, + + var previousMessageId: Int = -1, + + var token: String? = null, + + var threadId: Long? = null, + + var isThread: Boolean = false, + + var threadTitle: String? = null, + + var threadReplies: Int? = 0, + + // guests or users + var actorType: String? = null, + + var actorId: String? = null, + + // send when crafting a message + var actorDisplayName: String? = null, + + var timestamp: Long = 0, + + // send when crafting a message, max 1000 lines + var message: String? = null, + + var messageParameters: HashMap>? = null, + + var systemMessageType: SystemMessageType? = null, + + var replyable: Boolean = false, + + var parentMessageId: Long? = null, + + var readStatus: Enum = ReadStatus.NONE, + + var messageType: String? = null, + + var reactions: LinkedHashMap? = null, + + var reactionsSelf: ArrayList? = null, + + var expirationTimestamp: Int = 0, + + var renderMarkdown: Boolean? = null, + + var lastEditActorDisplayName: String? = null, + + var lastEditActorId: String? = null, + + var lastEditActorType: String? = null, + + var lastEditTimestamp: Long? = 0, + + var isDownloadingVoiceMessage: Boolean = false, + + var resetVoiceMessage: Boolean = false, + + var isPlayingVoiceMessage: Boolean = false, + + var wasPlayedVoiceMessage: Boolean = false, + + var voiceMessageDuration: Int = 0, + + var voiceMessagePlayedSeconds: Int = 0, + + var voiceMessageDownloadProgress: Int = 0, + + var voiceMessageSeekbarProgress: Int = 0, + + var voiceMessageFloatArray: FloatArray? = null, + + var expandableParent: Boolean = false, + + var isExpanded: Boolean = false, + + var lastItemOfExpandableGroup: Int = 0, + + var expandableChildrenAmount: Int = 0, + + var hiddenByCollapse: Boolean = false, + + var openWhenDownloaded: Boolean = true, + + var isTemporary: Boolean = false, + + var referenceId: String? = null, + + var sendStatus: SendStatus? = null, + + var silent: Boolean = false + +) : MessageContentType, + MessageContentType.Image { + + var extractedUrlToPreview: String? = null + + // messageTypesToIgnore is weird. must be deleted by refactoring!!! + @JsonIgnore + var messageTypesToIgnore = listOf( + MessageType.REGULAR_TEXT_MESSAGE, + MessageType.SYSTEM_MESSAGE, + MessageType.SINGLE_LINK_VIDEO_MESSAGE, + MessageType.SINGLE_LINK_AUDIO_MESSAGE, + MessageType.SINGLE_LINK_MESSAGE, + MessageType.SINGLE_NC_GEOLOCATION_MESSAGE, + MessageType.VOICE_MESSAGE, + MessageType.POLL_MESSAGE, + MessageType.DECK_CARD + ) + + fun isDeckCard(): Boolean { + if (messageParameters != null && messageParameters!!.size > 0) { + for ((_, individualHashMap) in messageParameters!!) { + if (isHashMapEntryEqualTo(individualHashMap, "type", "deck-card")) { + return true + } + } + } + return false + } + + fun hasFileAttachment(): Boolean { + if (messageParameters != null && messageParameters!!.size > 0) { + for ((_, individualHashMap) in messageParameters!!) { + if (isHashMapEntryEqualTo(individualHashMap, "type", "file")) { + return true + } + } + } + return false + } + + fun hasGeoLocation(): Boolean { + if (messageParameters != null && messageParameters!!.size > 0) { + for ((_, individualHashMap) in messageParameters!!) { + if (isHashMapEntryEqualTo(individualHashMap, "type", "geo-location")) { + return true + } + } + } + return false + } + + fun isPoll(): Boolean { + if (messageParameters != null && messageParameters!!.size > 0) { + for ((_, individualHashMap) in messageParameters!!) { + if (isHashMapEntryEqualTo(individualHashMap, "type", "talk-poll")) { + return true + } + } + } + return false + } + + @Suppress("ReturnCount") + fun isLinkPreview(): Boolean { + if (CapabilitiesUtil.isLinkPreviewAvailable(activeUser!!)) { + val regexStringFromServer = activeUser?.capabilities?.coreCapability?.referenceRegex + + val regexFromServer = regexStringFromServer?.toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)) + val regexDefault = REGEX_STRING_DEFAULT.toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)) + + val messageCharSequence: CharSequence = StringBuffer(message!!) + + if (regexFromServer != null) { + val foundLinkInServerRegex = regexFromServer.containsMatchIn(messageCharSequence) + if (foundLinkInServerRegex) { + extractedUrlToPreview = regexFromServer.find(messageCharSequence)?.groups?.get(0)?.value?.trim() + return true + } + } + + val foundLinkInDefaultRegex = regexDefault.containsMatchIn(messageCharSequence) + if (foundLinkInDefaultRegex) { + extractedUrlToPreview = regexDefault.find(messageCharSequence)?.groups?.get(0)?.value?.trim() + return true + } + } + return false + } + + @Suppress("Detekt.NestedBlockDepth") + override fun getImageUrl(): String? { + if (messageParameters != null && messageParameters!!.size > 0) { + for ((_, individualHashMap) in messageParameters!!) { + if (isHashMapEntryEqualTo(individualHashMap, "type", "file")) { + // FIX-ME: this selectedIndividualHashMap stuff needs to be analyzed and most likely be refactored! + // it just feels wrong to fill this here inside getImageUrl() + selectedIndividualHashMap = individualHashMap + if (!isVoiceMessage) { + if (activeUser != null && activeUser!!.baseUrl != null) { + return ApiUtils.getUrlForFilePreviewWithFileId( + activeUser!!.baseUrl!!, + individualHashMap["id"]!!, + sharedApplication!!.resources.getDimensionPixelSize(R.dimen.maximum_file_preview_size) + ) + } else { + Log.e( + TAG, + "activeUser or activeUser.getBaseUrl() were null when trying to getImageUrl()" + ) + } + } + } + } + } + return if (!messageTypesToIgnore.contains(getCalculateMessageType())) { + message!!.trim() + } else { + null + } + } + + fun getCalculateMessageType(): MessageType = + if (!TextUtils.isEmpty(systemMessage)) { + MessageType.SYSTEM_MESSAGE + } else if (isVoiceMessage) { + MessageType.VOICE_MESSAGE + } else if (hasFileAttachment()) { + MessageType.SINGLE_NC_ATTACHMENT_MESSAGE + } else if (hasGeoLocation()) { + MessageType.SINGLE_NC_GEOLOCATION_MESSAGE + } else if (isPoll()) { + MessageType.POLL_MESSAGE + } else if (isDeckCard()) { + MessageType.DECK_CARD + } else { + MessageType.REGULAR_TEXT_MESSAGE + } + + override fun getId(): String = jsonMessageId.toString() + + override fun getText(): String = + if (message != null) { + getParsedMessage(message, messageParameters)!! + } else { + "" + } + + fun getNullsafeActorDisplayName() = + if (!TextUtils.isEmpty(actorDisplayName)) { + actorDisplayName + } else { + sharedApplication!!.getString(R.string.nc_guest) + } + + override fun getUser(): IUser = + object : IUser { + override fun getId(): String = "$actorType/$actorId" + + override fun getName(): String = + if (!TextUtils.isEmpty(actorDisplayName)) { + actorDisplayName!! + } else { + sharedApplication!!.getString(R.string.nc_guest) + } + + override fun getAvatar(): String? = + when { + activeUser == null -> { + null + } + + actorType == "users" -> { + ApiUtils.getUrlForAvatar(activeUser!!.baseUrl!!, actorId, true) + } + + actorType == "bridged" -> { + ApiUtils.getUrlForAvatar( + activeUser!!.baseUrl!!, + "bridge-bot", + true + ) + } + + else -> { + var apiId: String? = sharedApplication!!.getString(R.string.nc_guest) + if (!TextUtils.isEmpty(actorDisplayName)) { + apiId = actorDisplayName + } + ApiUtils.getUrlForGuestAvatar(activeUser!!.baseUrl!!, apiId, true) + } + } + } + + override fun getCreatedAt(): Date = Date(timestamp * MILLIES) + + override fun getSystemMessage(): String = EnumSystemMessageTypeConverter().convertToString(systemMessageType) + + private fun isHashMapEntryEqualTo(map: HashMap, key: String, searchTerm: String): Boolean = + map != null && MessageDigest.isEqual(map[key]!!.toByteArray(), searchTerm.toByteArray()) + + // needed a equals and hashcode function to fix detekt errors + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return false + } + + override fun hashCode(): Int = 0 + + val isVoiceMessage: Boolean + get() = "voice-message" == messageType + val isCommandMessage: Boolean + get() = "command" == messageType + val isDeletedCommentMessage: Boolean + get() = "comment_deleted" == messageType + + enum class MessageType { + REGULAR_TEXT_MESSAGE, + SYSTEM_MESSAGE, + SINGLE_LINK_GIPHY_MESSAGE, + SINGLE_LINK_TENOR_MESSAGE, + SINGLE_LINK_GIF_MESSAGE, + SINGLE_LINK_MESSAGE, + SINGLE_LINK_VIDEO_MESSAGE, + SINGLE_LINK_IMAGE_MESSAGE, + SINGLE_LINK_AUDIO_MESSAGE, + SINGLE_NC_ATTACHMENT_MESSAGE, + SINGLE_NC_GEOLOCATION_MESSAGE, + POLL_MESSAGE, + VOICE_MESSAGE, + DECK_CARD + } + + /** + * see https://nextcloud-talk.readthedocs.io/en/latest/chat/#system-messages + */ + enum class SystemMessageType { + DUMMY, + CONVERSATION_CREATED, + CONVERSATION_RENAMED, + DESCRIPTION_REMOVED, + DESCRIPTION_SET, + CALL_STARTED, + CALL_JOINED, + CALL_LEFT, + CALL_ENDED, + CALL_ENDED_EVERYONE, + CALL_MISSED, + CALL_TRIED, + READ_ONLY_OFF, + READ_ONLY, + LISTABLE_NONE, + LISTABLE_USERS, + LISTABLE_ALL, + LOBBY_NONE, + LOBBY_NON_MODERATORS, + LOBBY_OPEN_TO_EVERYONE, + GUESTS_ALLOWED, + GUESTS_DISALLOWED, + PASSWORD_SET, + PASSWORD_REMOVED, + USER_ADDED, + USER_REMOVED, + GROUP_ADDED, + GROUP_REMOVED, + CIRCLE_ADDED, + CIRCLE_REMOVED, + MODERATOR_PROMOTED, + MODERATOR_DEMOTED, + GUEST_MODERATOR_PROMOTED, + GUEST_MODERATOR_DEMOTED, + MESSAGE_DELETED, + MESSAGE_EDITED, + FILE_SHARED, + OBJECT_SHARED, + MATTERBRIDGE_CONFIG_ADDED, + MATTERBRIDGE_CONFIG_EDITED, + MATTERBRIDGE_CONFIG_REMOVED, + MATTERBRIDGE_CONFIG_ENABLED, + MATTERBRIDGE_CONFIG_DISABLED, + CLEARED_CHAT, + REACTION, + REACTION_DELETED, + REACTION_REVOKED, + POLL_VOTED, + POLL_CLOSED, + MESSAGE_EXPIRATION_ENABLED, + MESSAGE_EXPIRATION_DISABLED, + RECORDING_STARTED, + RECORDING_STOPPED, + AUDIO_RECORDING_STARTED, + AUDIO_RECORDING_STOPPED, + RECORDING_FAILED, + BREAKOUT_ROOMS_STARTED, + BREAKOUT_ROOMS_STOPPED, + AVATAR_SET, + AVATAR_REMOVED, + FEDERATED_USER_ADDED, + FEDERATED_USER_REMOVED, + PHONE_ADDED, + THREAD_CREATED + } + + companion object { + private const val TAG = "ChatMessage" + private const val MILLIES: Long = 1000L + + private const val REGEX_STRING_DEFAULT = + """(\s|\n|^)(https?:\/\/)((?:[-A-Z0-9+_]+\.)+[-A-Z]+(?:\/[-A-Z0-9+&@#%?=~_|!:,.;()]*)*)(\s|\n|$)""" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt new file mode 100644 index 0000000..637cf5d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt @@ -0,0 +1,81 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.chat.data.network + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.opengraph.Reference +import com.nextcloud.talk.models.json.reminder.Reminder +import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall +import io.reactivex.Observable +import retrofit2.Response + +@Suppress("LongParameterList", "TooManyFunctions") +interface ChatNetworkDataSource { + fun getRoom(user: User, roomToken: String): Observable + fun getCapabilities(user: User, roomToken: String): Observable + fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable + fun setReminder( + user: User, + roomToken: String, + messageId: String, + timeStamp: Int, + chatApiVersion: Int + ): Observable + + fun getReminder(user: User, roomToken: String, messageId: String, apiVersion: Int): Observable + fun deleteReminder(user: User, roomToken: String, messageId: String, apiVersion: Int): Observable + fun shareToNotes( + credentials: String, + url: String, + message: String, + displayName: String + ): Observable + + suspend fun checkForNoteToSelf(credentials: String, url: String): RoomOverall + + fun shareLocationToNotes( + credentials: String, + url: String, + objectType: String, + objectId: String, + metadata: String + ): Observable + + fun leaveRoom(credentials: String, url: String): Observable + suspend fun sendChatMessage( + credentials: String, + url: String, + message: String, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String, + threadTitle: String? + ): ChatOverallSingleMessage + + fun pullChatMessages(credentials: String, url: String, fieldMap: HashMap): Observable> + fun deleteChatMessage(credentials: String, url: String): Observable + fun createRoom(credentials: String, url: String, map: Map): Observable + fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int): Observable + suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage + suspend fun getOutOfOfficeStatusForUser(credentials: String, baseUrl: String, userId: String): UserAbsenceOverall + suspend fun getContextForChatMessage( + credentials: String, + baseUrl: String, + token: String, + messageId: String, + limit: Int + ): List + suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference? + suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt new file mode 100644 index 0000000..c5bcd51 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/OfflineFirstChatRepository.kt @@ -0,0 +1,1109 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.data.network + +import android.os.Bundle +import android.util.Log +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.ChatMessageRepository +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.database.dao.ChatBlocksDao +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.database.mappers.asEntity +import com.nextcloud.talk.data.database.mappers.asModel +import com.nextcloud.talk.data.database.model.ChatBlockEntity +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import com.nextcloud.talk.data.database.model.SendStatus +import com.nextcloud.talk.data.network.NetworkMonitor +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.extensions.toIntOrZero +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.models.json.chat.ChatOverall +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage +import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.message.SendMessageUtils +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.io.IOException +import javax.inject.Inject + +@Suppress("LargeClass", "TooManyFunctions") +class OfflineFirstChatRepository @Inject constructor( + private val chatDao: ChatMessagesDao, + private val chatBlocksDao: ChatBlocksDao, + private val network: ChatNetworkDataSource, + private val networkMonitor: NetworkMonitor, + userProvider: CurrentUserProviderNew +) : ChatMessageRepository { + + val currentUser: User = userProvider.currentUser.blockingGet() + + override val messageFlow: + Flow< + Triple< + Boolean, + Boolean, + List + > + > + get() = _messageFlow + + private val _messageFlow: + MutableSharedFlow< + Triple< + Boolean, + Boolean, + List + > + > = MutableSharedFlow() + + override val updateMessageFlow: Flow + get() = _updateMessageFlow + + private val _updateMessageFlow: + MutableSharedFlow = MutableSharedFlow() + + override val lastCommonReadFlow: + Flow + get() = _lastCommonReadFlow + + private val _lastCommonReadFlow: + MutableSharedFlow = MutableSharedFlow() + + override val lastReadMessageFlow: Flow + get() = _lastReadMessageFlow + + private val _lastReadMessageFlow: + MutableSharedFlow = MutableSharedFlow() + + override val generalUIFlow: Flow + get() = _generalUIFlow + + private val _generalUIFlow: MutableSharedFlow = MutableSharedFlow() + + override val removeMessageFlow: Flow + get() = _removeMessageFlow + + private val _removeMessageFlow: + MutableSharedFlow = MutableSharedFlow() + + private var newXChatLastCommonRead: Int? = null + private var itIsPaused = false + private lateinit var scope: CoroutineScope + + lateinit var internalConversationId: String + private lateinit var conversationModel: ConversationModel + private lateinit var credentials: String + private lateinit var urlForChatting: String + private var threadId: Long? = null + + override fun initData(credentials: String, urlForChatting: String, roomToken: String, threadId: Long?) { + internalConversationId = currentUser.id.toString() + "@" + roomToken + this.credentials = credentials + this.urlForChatting = urlForChatting + this.threadId = threadId + } + + override fun updateConversation(conversationModel: ConversationModel) { + this.conversationModel = conversationModel + } + + override fun initScopeAndLoadInitialMessages(withNetworkParams: Bundle) { + scope = CoroutineScope(Dispatchers.IO) + loadInitialMessages(withNetworkParams) + } + + private fun loadInitialMessages(withNetworkParams: Bundle): Job = + scope.launch { + Log.d(TAG, "---- loadInitialMessages ------------") + newXChatLastCommonRead = conversationModel.lastCommonReadMessage + + Log.d(TAG, "conversationModel.internalId: " + conversationModel.internalId) + Log.d(TAG, "conversationModel.lastReadMessage:" + conversationModel.lastReadMessage) + + var newestMessageIdFromDb = chatBlocksDao.getNewestMessageIdFromChatBlocks(internalConversationId, threadId) + Log.d(TAG, "newestMessageIdFromDb: $newestMessageIdFromDb") + + val weAlreadyHaveSomeOfflineMessages = newestMessageIdFromDb > 0 + val weHaveAtLeastTheLastReadMessage = newestMessageIdFromDb >= conversationModel.lastReadMessage.toLong() + Log.d(TAG, "weAlreadyHaveSomeOfflineMessages:$weAlreadyHaveSomeOfflineMessages") + Log.d(TAG, "weHaveAtLeastTheLastReadMessage:$weHaveAtLeastTheLastReadMessage") + + if (weAlreadyHaveSomeOfflineMessages && weHaveAtLeastTheLastReadMessage) { + Log.d( + TAG, + "Initial online request is skipped because offline messages are up to date" + + " until lastReadMessage" + ) + Log.d(TAG, "For messages newer than lastRead, lookIntoFuture will load them.") + } else { + if (!weAlreadyHaveSomeOfflineMessages) { + Log.d(TAG, "An online request for newest 100 messages is made because offline chat is empty") + if (networkMonitor.isOnline.value.not()) { + _generalUIFlow.emit(ChatActivity.NO_OFFLINE_MESSAGES_FOUND) + } + } else { + Log.d( + TAG, + "An online request for newest 100 messages is made because we don't have the lastReadMessage " + + "(gaps could be closed by scrolling up to merge the chatblocks)" + ) + } + + // set up field map to load the newest messages + val fieldMap = getFieldMap( + lookIntoFuture = false, + timeout = 0, + includeLastKnown = true, + setReadMarker = true, + lastKnown = null + ) + withNetworkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) + withNetworkParams.putString(BundleKeys.KEY_ROOM_TOKEN, conversationModel.token) + + Log.d(TAG, "Starting online request for initial loading") + val chatMessageEntities = sync(withNetworkParams) + if (chatMessageEntities == null) { + Log.e(TAG, "initial loading of messages failed") + } + + newestMessageIdFromDb = chatBlocksDao.getNewestMessageIdFromChatBlocks(internalConversationId, threadId) + Log.d(TAG, "newestMessageIdFromDb after sync: $newestMessageIdFromDb") + } + + handleMessagesFromDb(newestMessageIdFromDb) + + initMessagePolling(newestMessageIdFromDb) + } + + private suspend fun handleMessagesFromDb(newestMessageIdFromDb: Long) { + if (newestMessageIdFromDb.toInt() != 0) { + val limit = getCappedMessagesAmountOfChatBlock(newestMessageIdFromDb) + + val list = getMessagesBeforeAndEqual( + messageId = newestMessageIdFromDb, + internalConversationId = internalConversationId, + messageLimit = limit + ) + if (list.isNotEmpty()) { + handleNewAndTempMessages( + receivedChatMessages = list, + lookIntoFuture = false, + showUnreadMessagesMarker = false + ) + } + + // this call could be deleted when we have a worker to send messages.. + sendUnsentChatMessages(credentials, urlForChatting) + + // delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing + // with them (otherwise there is a race condition). + delay(DELAY_TO_ENSURE_MESSAGES_ARE_ADDED) + + updateUiForLastCommonRead() + updateUiForLastReadMessage(newestMessageIdFromDb) + } + } + + private suspend fun getCappedMessagesAmountOfChatBlock(messageId: Long): Int { + val chatBlock = getBlockOfMessage(messageId.toInt()) + + if (chatBlock != null) { + val amountBetween = chatDao.getCountBetweenMessageIds( + internalConversationId, + messageId, + chatBlock.oldestMessageId, + threadId + ) + + Log.d(TAG, "amount of messages between newestMessageId and oldest message of same ChatBlock:$amountBetween") + val limit = if (amountBetween > DEFAULT_MESSAGES_LIMIT) { + DEFAULT_MESSAGES_LIMIT + } else { + amountBetween + } + Log.d(TAG, "limit of messages to load for UI (max 100 to ensure performance is okay):$limit") + return limit + } else { + Log.e(TAG, "No chat block found. Returning 0 as limit.") + return 0 + } + } + + private suspend fun updateUiForLastReadMessage(newestMessageId: Long) { + val scrollToLastRead = conversationModel.lastReadMessage.toLong() < newestMessageId + if (scrollToLastRead) { + _lastReadMessageFlow.emit(conversationModel.lastReadMessage) + } + } + + private fun updateUiForLastCommonRead() { + scope.launch { + newXChatLastCommonRead?.let { + _lastCommonReadFlow.emit(it) + } + } + } + + override fun loadMoreMessages( + beforeMessageId: Long, + roomToken: String, + withMessageLimit: Int, + withNetworkParams: Bundle + ): Job = + scope.launch { + Log.d(TAG, "---- loadMoreMessages for $beforeMessageId ------------") + + val fieldMap = getFieldMap( + lookIntoFuture = false, + timeout = 0, + includeLastKnown = false, + setReadMarker = true, + lastKnown = beforeMessageId.toInt() + ) + withNetworkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) + + val loadFromServer = hasToLoadPreviousMessagesFromServer(beforeMessageId, DEFAULT_MESSAGES_LIMIT) + + if (loadFromServer) { + Log.d(TAG, "Starting online request for loadMoreMessages") + sync(withNetworkParams) + } + + showMessagesBefore(internalConversationId, beforeMessageId, DEFAULT_MESSAGES_LIMIT) + updateUiForLastCommonRead() + } + + override fun initMessagePolling(initialMessageId: Long): Job = + scope.launch { + Log.d(TAG, "---- initMessagePolling ------------") + + Log.d(TAG, "newestMessage: $initialMessageId") + + var fieldMap = getFieldMap( + lookIntoFuture = true, + // timeout for first longpoll is 0, so "unread message" info is not shown if there were + // initially no messages but someone writes us in the first 30 seconds. + timeout = 0, + includeLastKnown = false, + setReadMarker = true, + lastKnown = initialMessageId.toInt() + ) + + val networkParams = Bundle() + + var showUnreadMessagesMarker = true + + while (isActive) { + if (!networkMonitor.isOnline.value || itIsPaused) { + Thread.sleep(HALF_SECOND) + } else { + // sync database with server + // (This is a long blocking call because long polling (lookIntoFuture) is set) + networkParams.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) + + Log.d(TAG, "Starting online request for long polling") + val resultsFromSync = sync(networkParams) + if (!resultsFromSync.isNullOrEmpty()) { + val chatMessages = resultsFromSync.map(ChatMessageEntity::asModel) + + val weHaveMessagesFromOurself = chatMessages.any { it.actorId == currentUser.userId } + showUnreadMessagesMarker = showUnreadMessagesMarker && !weHaveMessagesFromOurself + + if (isActive) { + handleNewAndTempMessages( + receivedChatMessages = chatMessages, + lookIntoFuture = true, + showUnreadMessagesMarker = showUnreadMessagesMarker + ) + } else { + Log.d(TAG, "scope was already canceled") + } + } else { + Log.d(TAG, "resultsFromSync are null or empty") + } + + updateUiForLastCommonRead() + + val newestMessage = chatBlocksDao.getNewestMessageIdFromChatBlocks( + internalConversationId, + threadId + ).toInt() + + // update field map vars for next cycle + fieldMap = getFieldMap( + lookIntoFuture = true, + timeout = 30, + includeLastKnown = false, + setReadMarker = true, + lastKnown = newestMessage + ) + + showUnreadMessagesMarker = false + } + } + } + + private suspend fun handleNewAndTempMessages( + receivedChatMessages: List, + lookIntoFuture: Boolean, + showUnreadMessagesMarker: Boolean + ) { + receivedChatMessages.forEach { + Log.d(TAG, "receivedChatMessage: " + it.message) + } + + // remove all temp messages from UI + val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId) + .first() + .map(ChatMessageEntity::asModel) + oldTempMessages.forEach { + Log.d(TAG, "oldTempMessage to be removed from UI: " + it.message) + _removeMessageFlow.emit(it) + } + + // add new messages to UI + val tripleChatMessages = Triple(lookIntoFuture, showUnreadMessagesMarker, receivedChatMessages) + _messageFlow.emit(tripleChatMessages) + + // remove temp messages from DB that are now found in the new messages + val chatMessagesReferenceIds = receivedChatMessages.mapTo(HashSet(receivedChatMessages.size)) { it.referenceId } + val tempChatMessagesThatCanBeReplaced = oldTempMessages.filter { it.referenceId in chatMessagesReferenceIds } + tempChatMessagesThatCanBeReplaced.forEach { + Log.d(TAG, "oldTempMessage that was identified in newMessages: " + it.message) + } + chatDao.deleteTempChatMessages( + internalConversationId, + tempChatMessagesThatCanBeReplaced.map { it.referenceId!! } + ) + + // add the remaining temp messages to UI again + val remainingTempMessages = chatDao.getTempMessagesForConversation(internalConversationId) + .first() + .sortedBy { it.internalId } + .map(ChatMessageEntity::asModel) + + remainingTempMessages.forEach { + Log.d(TAG, "remainingTempMessage: " + it.message) + } + + val triple = Triple(true, false, remainingTempMessages) + _messageFlow.emit(triple) + } + + private suspend fun hasToLoadPreviousMessagesFromServer(beforeMessageId: Long, amountToCheck: Int): Boolean { + val loadFromServer: Boolean + + val blockForMessage = getBlockOfMessage(beforeMessageId.toInt()) + + if (blockForMessage == null) { + Log.d(TAG, "No blocks for this message were found so we have to ask server") + loadFromServer = true + } else if (!blockForMessage.hasHistory) { + Log.d(TAG, "The last chatBlock is reached so we won't request server for older messages") + loadFromServer = false + } else { + val amountBetween = chatDao.getCountBetweenMessageIds( + internalConversationId, + beforeMessageId, + blockForMessage.oldestMessageId, + threadId + ) + loadFromServer = amountBetween < amountToCheck + + Log.d( + TAG, + "Amount between messageId " + beforeMessageId + " and " + blockForMessage.oldestMessageId + + " is: " + amountBetween + " and $amountToCheck were needed, so 'loadFromServer' is " + + loadFromServer + ) + } + return loadFromServer + } + + @Suppress("LongParameterList") + private fun getFieldMap( + lookIntoFuture: Boolean, + timeout: Int, + includeLastKnown: Boolean, + setReadMarker: Boolean, + lastKnown: Int?, + limit: Int = DEFAULT_MESSAGES_LIMIT + ): HashMap { + val fieldMap = HashMap() + + fieldMap["includeLastKnown"] = if (includeLastKnown) 1 else 0 + + if (lastKnown != null) { + fieldMap["lastKnownMessageId"] = lastKnown + } + + newXChatLastCommonRead?.let { + fieldMap["lastCommonReadId"] = it + } + + threadId?.let { fieldMap["threadId"] = it.toInt() } + + fieldMap["timeout"] = timeout + fieldMap["limit"] = limit + + fieldMap["lookIntoFuture"] = if (lookIntoFuture) 1 else 0 + fieldMap["setReadMarker"] = if (setReadMarker) 1 else 0 + + return fieldMap + } + + override suspend fun getNumberOfThreadReplies(threadId: Long): Int = + chatDao.getNumberOfThreadReplies(internalConversationId, threadId) + + override suspend fun getMessage(messageId: Long, bundle: Bundle): Flow { + Log.d(TAG, "Get message with id $messageId") + val loadFromServer = hasToLoadPreviousMessagesFromServer(messageId, 1) + + if (loadFromServer) { + val fieldMap = getFieldMap( + lookIntoFuture = false, + timeout = 0, + includeLastKnown = true, + setReadMarker = false, + lastKnown = messageId.toInt(), + limit = 1 + ) + bundle.putSerializable(BundleKeys.KEY_FIELD_MAP, fieldMap) + + Log.d(TAG, "Starting online request for single message (e.g. a reply)") + sync(bundle) + } + return chatDao.getChatMessageForConversation( + internalConversationId, + messageId + ).map(ChatMessageEntity::asModel) + } + + @Suppress("UNCHECKED_CAST", "MagicNumber", "Detekt.TooGenericExceptionCaught") + private fun getMessagesFromServer(bundle: Bundle): Pair>? { + val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap + + var attempts = 1 + while (attempts < 5) { + Log.d(TAG, "message limit: " + fieldMap["limit"]) + try { + val result = network.pullChatMessages(credentials, urlForChatting, fieldMap) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .map { it -> + when (it.code()) { + HTTP_CODE_OK -> { + Log.d(TAG, "getMessagesFromServer HTTP_CODE_OK") + newXChatLastCommonRead = it.headers()["X-Chat-Last-Common-Read"]?.let { + Integer.parseInt(it) + } + + return@map Pair( + HTTP_CODE_OK, + (it.body() as ChatOverall).ocs!!.data!! + ) + } + + HTTP_CODE_NOT_MODIFIED -> { + Log.d(TAG, "getMessagesFromServer HTTP_CODE_NOT_MODIFIED") + + return@map Pair( + HTTP_CODE_NOT_MODIFIED, + listOf() + ) + } + + HTTP_CODE_PRECONDITION_FAILED -> { + Log.d(TAG, "getMessagesFromServer HTTP_CODE_PRECONDITION_FAILED") + + return@map Pair( + HTTP_CODE_PRECONDITION_FAILED, + listOf() + ) + } + + else -> { + return@map Pair( + HTTP_CODE_PRECONDITION_FAILED, + listOf() + ) + } + } + } + .blockingSingle() + return result + } catch (e: Exception) { + Log.e(TAG, "Something went wrong when pulling chat messages (attempt: $attempts)", e) + attempts++ + + val newMessageLimit = when (attempts) { + 2 -> 50 + 3 -> 10 + else -> 5 + } + fieldMap["limit"] = newMessageLimit + } + } + Log.e(TAG, "All attempts to get messages from server failed") + return null + } + + private suspend fun sync(bundle: Bundle): List? { + if (!networkMonitor.isOnline.value) { + Log.d(TAG, "Device is offline, can't load chat messages from server") + return null + } + + val result = getMessagesFromServer(bundle) + if (result == null) { + Log.d(TAG, "No result from server") + return null + } + + var chatMessagesFromSync: List? = null + + val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap + val queriedMessageId = fieldMap["lastKnownMessageId"] + val lookIntoFuture = fieldMap["lookIntoFuture"] == 1 + + val statusCode = result.first + + val hasHistory = getHasHistory(statusCode, lookIntoFuture) + + Log.d( + TAG, + "internalConv=$internalConversationId statusCode=$statusCode lookIntoFuture=$lookIntoFuture " + + "hasHistory=$hasHistory " + + "queriedMessageId=$queriedMessageId" + ) + + val blockContainingQueriedMessage: ChatBlockEntity? = getBlockOfMessage(queriedMessageId) + + if (blockContainingQueriedMessage != null && !hasHistory) { + blockContainingQueriedMessage.hasHistory = false + chatBlocksDao.upsertChatBlock(blockContainingQueriedMessage) + Log.d(TAG, "End of chat was reached so hasHistory=false is set") + } + + if (result.second.isNotEmpty()) { + chatMessagesFromSync = updateMessagesData( + result.second, + blockContainingQueriedMessage, + lookIntoFuture, + hasHistory + ) + } else { + Log.d(TAG, "no data is updated...") + } + + return chatMessagesFromSync + } + + private suspend fun OfflineFirstChatRepository.updateMessagesData( + chatMessagesJson: List, + blockContainingQueriedMessage: ChatBlockEntity?, + lookIntoFuture: Boolean, + hasHistory: Boolean + ): List { + handleUpdateMessages(chatMessagesJson) + + val chatMessagesFromSyncToProcess = chatMessagesJson.map { + it.asEntity(currentUser.id!!) + } + + chatDao.upsertChatMessages(chatMessagesFromSyncToProcess) + + val oldestIdFromSync = chatMessagesFromSyncToProcess.minByOrNull { it.id }!!.id + val newestIdFromSync = chatMessagesFromSyncToProcess.maxByOrNull { it.id }!!.id + Log.d(TAG, "oldestIdFromSync: $oldestIdFromSync") + Log.d(TAG, "newestIdFromSync: $newestIdFromSync") + + var oldestMessageIdForNewChatBlock = oldestIdFromSync + var newestMessageIdForNewChatBlock = newestIdFromSync + + if (blockContainingQueriedMessage != null) { + if (lookIntoFuture) { + val oldestMessageIdFromBlockOfQueriedMessage = blockContainingQueriedMessage.oldestMessageId + Log.d(TAG, "oldestMessageIdFromBlockOfQueriedMessage: $oldestMessageIdFromBlockOfQueriedMessage") + oldestMessageIdForNewChatBlock = oldestMessageIdFromBlockOfQueriedMessage + } else { + val newestMessageIdFromBlockOfQueriedMessage = blockContainingQueriedMessage.newestMessageId + Log.d(TAG, "newestMessageIdFromBlockOfQueriedMessage: $newestMessageIdFromBlockOfQueriedMessage") + newestMessageIdForNewChatBlock = newestMessageIdFromBlockOfQueriedMessage + } + } + + Log.d(TAG, "oldestMessageIdForNewChatBlock: $oldestMessageIdForNewChatBlock") + Log.d(TAG, "newestMessageIdForNewChatBlock: $newestMessageIdForNewChatBlock") + + val newChatBlock = ChatBlockEntity( + internalConversationId = internalConversationId, + accountId = conversationModel.accountId, + token = conversationModel.token, + threadId = threadId, + oldestMessageId = oldestMessageIdForNewChatBlock, + newestMessageId = newestMessageIdForNewChatBlock, + hasHistory = hasHistory + ) + chatBlocksDao.upsertChatBlock(newChatBlock) // crash when no conversation thread exists! + + updateBlocks(newChatBlock) + return chatMessagesFromSyncToProcess + } + + private suspend fun handleUpdateMessages(messagesJson: List) { + messagesJson.forEach { messageJson -> + when (messageJson.systemMessageType) { + ChatMessage.SystemMessageType.REACTION, + ChatMessage.SystemMessageType.REACTION_REVOKED, + ChatMessage.SystemMessageType.REACTION_DELETED, + ChatMessage.SystemMessageType.MESSAGE_DELETED, + ChatMessage.SystemMessageType.POLL_VOTED, + ChatMessage.SystemMessageType.MESSAGE_EDITED -> { + // the parent message is always the newest state, no matter how old the system message is. + // that's why we can just take the parent, update it in DB and update the UI + messageJson.parentMessage?.let { parentMessageJson -> + parentMessageJson.message?.let { + val parentMessageEntity = parentMessageJson.asEntity(currentUser.id!!) + chatDao.upsertChatMessage(parentMessageEntity) + _updateMessageFlow.emit(parentMessageEntity.asModel()) + } + } + } + + ChatMessage.SystemMessageType.CLEARED_CHAT -> { + // for lookIntoFuture just deleting everything would be fine. + // But lets say we did not open the chat for a while and in between it was cleared. + // We just load the last messages but this don't contain the system message. + // We scroll up and load the system message. Deleting everything is not an option as we + // would loose the messages that we want to keep. We only want to + // delete the messages and chatBlocks older than the system message. + chatDao.deleteMessagesOlderThan(internalConversationId, messageJson.id) + chatBlocksDao.deleteChatBlocksOlderThan(internalConversationId, messageJson.id) + } + + else -> {} + } + } + } + + /** + * 304 is returned when oldest message of chat was queried or when long polling request returned with no + * modification. hasHistory is only set to false, when 304 was returned for the the oldest message + */ + private fun getHasHistory(statusCode: Int, lookIntoFuture: Boolean): Boolean = + if (statusCode == HTTP_CODE_NOT_MODIFIED) { + lookIntoFuture + } else { + true + } + + private suspend fun getBlockOfMessage(queriedMessageId: Int?): ChatBlockEntity? { + var blockContainingQueriedMessage: ChatBlockEntity? = null + if (queriedMessageId != null) { + val blocksContainingQueriedMessage = + chatBlocksDao.getChatBlocksContainingMessageId( + internalConversationId = internalConversationId, + threadId = threadId, + messageId = queriedMessageId.toLong() + ) + + val chatBlocks = blocksContainingQueriedMessage.first() + if (chatBlocks.size > 1) { + Log.w(TAG, "multiple chat blocks with messageId $queriedMessageId were found") + } + + blockContainingQueriedMessage = if (chatBlocks.isNotEmpty()) { + chatBlocks.first() + } else { + null + } + } + return blockContainingQueriedMessage + } + + private suspend fun updateBlocks(chatBlock: ChatBlockEntity): ChatBlockEntity? { + val connectedChatBlocks = + chatBlocksDao.getConnectedChatBlocks( + internalConversationId = internalConversationId, + threadId = threadId, + oldestMessageId = chatBlock.oldestMessageId, + newestMessageId = chatBlock.newestMessageId + ).first() + + return if (connectedChatBlocks.size == 1) { + Log.d(TAG, "This chatBlock is not connected to others") + val chatBlockFromDb = connectedChatBlocks[0] + Log.d(TAG, "chatBlockFromDb.oldestMessageId: " + chatBlockFromDb.oldestMessageId) + Log.d(TAG, "chatBlockFromDb.newestMessageId: " + chatBlockFromDb.newestMessageId) + chatBlockFromDb + } else if (connectedChatBlocks.size > 1) { + Log.d(TAG, "Found " + connectedChatBlocks.size + " chat blocks that are connected") + val oldestIdFromDbChatBlocks = + connectedChatBlocks.minByOrNull { it.oldestMessageId }!!.oldestMessageId + val newestIdFromDbChatBlocks = + connectedChatBlocks.maxByOrNull { it.newestMessageId }!!.newestMessageId + + val hasNoHistory = connectedChatBlocks.any { !it.hasHistory } + val hasHistory = !hasNoHistory + Log.d(TAG, "hasHistory = $hasHistory") + + chatBlocksDao.deleteChatBlocks(connectedChatBlocks) + Log.d(TAG, "These chat blocks were deleted") + + val newChatBlock = ChatBlockEntity( + internalConversationId = internalConversationId, + accountId = conversationModel.accountId, + token = conversationModel.token, + threadId = threadId, + oldestMessageId = oldestIdFromDbChatBlocks, + newestMessageId = newestIdFromDbChatBlocks, + hasHistory = hasHistory + ) + chatBlocksDao.upsertChatBlock(newChatBlock) + Log.d(TAG, "A new chat block was created that covers all the range of the found chatblocks") + Log.d(TAG, "new chatBlock - oldest MessageId: $oldestIdFromDbChatBlocks") + Log.d(TAG, "new chatBlock - newest MessageId: $newestIdFromDbChatBlocks") + newChatBlock + } else { + Log.d(TAG, "No chat block found ....") + null + } + } + + suspend fun getMessagesBeforeAndEqual( + messageId: Long, + internalConversationId: String, + messageLimit: Int + ): List = + chatDao.getMessagesForConversationBeforeAndEqual( + internalConversationId, + messageId, + messageLimit, + threadId + ).map { + it.map(ChatMessageEntity::asModel) + }.first() + + private suspend fun showMessagesBefore(internalConversationId: String, messageId: Long, limit: Int) { + suspend fun getMessagesBefore( + messageId: Long, + internalConversationId: String, + messageLimit: Int + ): List = + chatDao.getMessagesForConversationBefore( + internalConversationId, + messageId, + messageLimit, + threadId + ).map { + it.map(ChatMessageEntity::asModel) + }.first() + + val list = getMessagesBefore( + messageId, + internalConversationId, + limit + ) + + if (list.isNotEmpty()) { + val triple = Triple(false, false, list) + _messageFlow.emit(triple) + } + } + + override fun handleOnPause() { + itIsPaused = true + if (this::scope.isInitialized) { + scope.cancel() + } + } + + override fun handleOnResume() { + itIsPaused = false + } + + override fun handleOnStop() { + // not used + } + + @Suppress("LongParameterList") + override suspend fun sendChatMessage( + credentials: String, + url: String, + message: String, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String, + threadTitle: String? + ): Flow> { + if (!networkMonitor.isOnline.value) { + return flow { + emit(Result.failure(IOException("Skipped to send message as device is offline"))) + } + } + + return flow { + val response = network.sendChatMessage( + credentials, + url, + message, + displayName, + replyTo, + sendWithoutNotification, + referenceId, + threadTitle + ) + + val chatMessageModel = response.ocs?.data?.asModel() + + val sentMessage = chatDao.getTempMessageForConversation( + internalConversationId, + referenceId, + threadId + ).firstOrNull() + + sentMessage?.let { + it.sendStatus = SendStatus.SENT_PENDING_ACK + chatDao.updateChatMessage(it) + } + + Log.d(TAG, "sending chat message succeeded: " + message) + emit(Result.success(chatMessageModel)) + } + .catch { e -> + Log.e(TAG, "Error when sending message", e) + + val failedMessage = chatDao.getTempMessageForConversation( + internalConversationId, + referenceId, + threadId + ).firstOrNull() + failedMessage?.let { + it.sendStatus = SendStatus.FAILED + chatDao.updateChatMessage(it) + + val failedMessageModel = it.asModel() + _updateMessageFlow.emit(failedMessageModel) + } + emit(Result.failure(e)) + } + } + + @Suppress("LongParameterList") + override suspend fun resendChatMessage( + credentials: String, + url: String, + message: String, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String + ): Flow> { + val messageToResend = chatDao.getTempMessageForConversation( + internalConversationId, + referenceId, + threadId + ).firstOrNull() + return if (messageToResend != null) { + messageToResend.sendStatus = SendStatus.PENDING + chatDao.updateChatMessage(messageToResend) + + val messageToResendModel = messageToResend.asModel() + _updateMessageFlow.emit(messageToResendModel) + + sendChatMessage( + credentials = credentials, + url = url, + message = message, + displayName = displayName, + replyTo = replyTo, + sendWithoutNotification = sendWithoutNotification, + referenceId = referenceId, + threadTitle = null + ) + } else { + flow { + emit(Result.failure(IllegalStateException("No temporary message found to resend"))) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + override suspend fun editChatMessage( + credentials: String, + url: String, + text: String + ): Flow> = + flow { + try { + val response = network.editChatMessage( + credentials, + url, + text + ) + emit(Result.success(response)) + } catch (e: Exception) { + emit(Result.failure(e)) + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + override suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow = + flow { + try { + val messageToEdit = chatDao.getChatMessageForConversation( + internalConversationId, + message.jsonMessageId.toLong() + ).first() + messageToEdit.message = editedMessageText + chatDao.upsertChatMessage(messageToEdit) + + val editedMessageModel = messageToEdit.asModel() + _updateMessageFlow.emit(editedMessageModel) + emit(true) + } catch (e: Exception) { + emit(false) + } + } + + override suspend fun sendUnsentChatMessages(credentials: String, url: String) { + val tempMessages = chatDao.getTempUnsentMessagesForConversation(internalConversationId, threadId).first() + tempMessages.sortedBy { it.internalId }.onEach { + sendChatMessage( + credentials, + url, + it.message, + it.actorDisplayName, + it.parentMessageId?.toIntOrZero() ?: 0, + it.silent, + it.referenceId.orEmpty(), + null + ).collect { result -> + if (result.isSuccess) { + Log.d(TAG, "Sent temp message") + } else { + Log.e(TAG, "Failed to send temp message") + } + } + } + } + + override suspend fun deleteTempMessage(chatMessage: ChatMessage) { + chatDao.deleteTempChatMessages(internalConversationId, listOf(chatMessage.referenceId.orEmpty())) + _removeMessageFlow.emit(chatMessage) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + override suspend fun addTemporaryMessage( + message: CharSequence, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String + ): Flow> = + flow { + try { + val tempChatMessageEntity = createChatMessageEntity( + internalConversationId, + message.toString(), + replyTo, + sendWithoutNotification, + referenceId + ) + + chatDao.upsertChatMessage(tempChatMessageEntity) + + val tempChatMessageModel = tempChatMessageEntity.asModel() + + emit(Result.success(tempChatMessageModel)) + + val triple = Triple(true, false, listOf(tempChatMessageModel)) + _messageFlow.emit(triple) + } catch (e: Exception) { + Log.e(TAG, "Something went wrong when adding temporary message", e) + emit(Result.failure(e)) + } + } + + private fun createChatMessageEntity( + internalConversationId: String, + message: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String + ): ChatMessageEntity { + val currentTimeMillies = System.currentTimeMillis() + + val currentTimeWithoutYear = SendMessageUtils().removeYearFromTimestamp(currentTimeMillies) + + val parentMessageId = if (replyTo != 0) { + replyTo.toLong() + } else { + null + } + + val entity = ChatMessageEntity( + internalId = "$internalConversationId@_temp_$currentTimeMillies", + internalConversationId = internalConversationId, + id = currentTimeWithoutYear.toLong(), + threadId = threadId, + message = message, + deleted = false, + token = conversationModel.token, + actorId = currentUser.userId!!, + actorType = EnumActorTypeConverter().convertToString(Participant.ActorType.USERS), + accountId = currentUser.id!!, + messageParameters = null, + messageType = "comment", + parentMessageId = parentMessageId, + systemMessageType = ChatMessage.SystemMessageType.DUMMY, + replyable = false, + timestamp = currentTimeMillies / MILLIES, + expirationTimestamp = 0, + actorDisplayName = currentUser.displayName!!, + referenceId = referenceId, + isTemporary = true, + sendStatus = SendStatus.PENDING, + silent = sendWithoutNotification + ) + return entity + } + + companion object { + val TAG = OfflineFirstChatRepository::class.simpleName + private const val HTTP_CODE_OK: Int = 200 + private const val HTTP_CODE_NOT_MODIFIED = 304 + private const val HTTP_CODE_PRECONDITION_FAILED = 412 + private const val HALF_SECOND = 500L + private const val DELAY_TO_ENSURE_MESSAGES_ARE_ADDED: Long = 100 + private const val DEFAULT_MESSAGES_LIMIT = 100 + private const val MILLIES = 1000 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt new file mode 100644 index 0000000..057472f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt @@ -0,0 +1,224 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.chat.data.network + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.opengraph.Reference +import com.nextcloud.talk.models.json.reminder.Reminder +import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.message.SendMessageUtils +import io.reactivex.Observable +import retrofit2.Response + +class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines: NcApiCoroutines) : + ChatNetworkDataSource { + override fun getRoom(user: User, roomToken: String): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) + + return ncApi.getRoom( + credentials, + ApiUtils.getUrlForRoom(apiVersion, user.baseUrl!!, roomToken) + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) } + } + + override fun getCapabilities(user: User, roomToken: String): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) + + return ncApi.getRoomCapabilities( + credentials, + ApiUtils.getUrlForRoomCapabilities(apiVersion, user.baseUrl!!, roomToken) + ).map { it.ocs?.data } + } + + override fun joinRoom(user: User, roomToken: String, roomPassword: String): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, 1)) + + return ncApi.joinRoom( + credentials, + ApiUtils.getUrlForParticipantsActive(apiVersion, user.baseUrl!!, roomToken), + roomPassword + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) } + } + + override fun setReminder( + user: User, + roomToken: String, + messageId: String, + timeStamp: Int, + chatApiVersion: Int + ): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + return ncApi.setReminder( + credentials, + ApiUtils.getUrlForReminder(user, roomToken, messageId, chatApiVersion), + timeStamp + ).map { + it.ocs!!.data + } + } + + override fun getReminder( + user: User, + roomToken: String, + messageId: String, + chatApiVersion: Int + ): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + return ncApi.getReminder( + credentials, + ApiUtils.getUrlForReminder(user, roomToken, messageId, chatApiVersion) + ).map { + it.ocs!!.data + } + } + + override fun deleteReminder( + user: User, + roomToken: String, + messageId: String, + chatApiVersion: Int + ): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + return ncApi.deleteReminder( + credentials, + ApiUtils.getUrlForReminder(user, roomToken, messageId, chatApiVersion) + ).map { + it + } + } + + override fun shareToNotes( + credentials: String, + url: String, + message: String, + displayName: String + ): Observable = + ncApi.sendChatMessage( + credentials, + url, + message, + displayName, + null, + false, + SendMessageUtils().generateReferenceId() + ).map { + it + } + + override suspend fun checkForNoteToSelf(credentials: String, url: String): RoomOverall = + ncApiCoroutines.getNoteToSelfRoom(credentials, url) + + override fun shareLocationToNotes( + credentials: String, + url: String, + objectType: String, + objectId: String, + metadata: String + ): Observable = ncApi.sendLocation(credentials, url, objectType, objectId, metadata).map { it } + + override fun leaveRoom(credentials: String, url: String): Observable = + ncApi.leaveRoom(credentials, url).map { + it + } + + override suspend fun sendChatMessage( + credentials: String, + url: String, + message: String, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + referenceId: String, + threadTitle: String? + ): ChatOverallSingleMessage = + ncApiCoroutines.sendChatMessage( + credentials, + url, + message, + displayName, + replyTo, + sendWithoutNotification, + referenceId, + threadTitle + ) + + override fun pullChatMessages( + credentials: String, + url: String, + fieldMap: HashMap + ): Observable> = ncApi.pullChatMessages(credentials, url, fieldMap).map { it } + + override fun deleteChatMessage(credentials: String, url: String): Observable = + ncApi.deleteChatMessage(credentials, url).map { + it + } + + override fun createRoom(credentials: String, url: String, map: Map): Observable = + ncApi.createRoom(credentials, url, map).map { + it + } + + override fun setChatReadMarker( + credentials: String, + url: String, + previousMessageId: Int + ): Observable = ncApi.setChatReadMarker(credentials, url, previousMessageId).map { it } + + override suspend fun editChatMessage(credentials: String, url: String, text: String): ChatOverallSingleMessage = + ncApiCoroutines.editChatMessage(credentials, url, text) + + override suspend fun getOutOfOfficeStatusForUser( + credentials: String, + baseUrl: String, + userId: String + ): UserAbsenceOverall = + ncApiCoroutines.getOutOfOfficeStatusForUser( + credentials, + ApiUtils.getUrlForOutOfOffice(baseUrl, userId) + ) + + override suspend fun getContextForChatMessage( + credentials: String, + baseUrl: String, + token: String, + messageId: String, + limit: Int + ): List { + val url = ApiUtils.getUrlForChatMessageContext(baseUrl, token, messageId) + return ncApiCoroutines.getContextOfChatMessage(credentials, url, limit).ocs?.data ?: listOf() + } + + override suspend fun getOpenGraph( + credentials: String, + baseUrl: String, + extractedLinkToPreview: String + ): Reference? { + val openGraphLink = ApiUtils.getUrlForOpenGraph(baseUrl) + return ncApi.getOpenGraph( + credentials, + openGraphLink, + extractedLinkToPreview + ).blockingFirst().ocs?.data?.references?.entries?.iterator()?.next()?.value + } + + override suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall { + val url = ApiUtils.getUrlForUnbindingRoom(baseUrl, roomToken) + return ncApiCoroutines.unbindRoom(credentials, url) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt new file mode 100644 index 0000000..7b8f6dc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -0,0 +1,1012 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.chat.viewmodels + +import android.content.Context +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.gson.Gson +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.chat.data.ChatMessageRepository +import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager +import com.nextcloud.talk.chat.data.io.MediaPlayerManager +import com.nextcloud.talk.chat.data.io.MediaRecorderManager +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository +import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel.Companion.FOLLOWED_THREADS_EXIST +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.extensions.toIntOrZero +import com.nextcloud.talk.jobs.UploadAndShareFilesWorker +import com.nextcloud.talk.models.MessageDraft +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.domain.ReactionAddedModel +import com.nextcloud.talk.models.domain.ReactionDeletedModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.opengraph.Reference +import com.nextcloud.talk.models.json.reminder.Reminder +import com.nextcloud.talk.models.json.threads.ThreadInfo +import com.nextcloud.talk.models.json.userAbsence.UserAbsenceData +import com.nextcloud.talk.repositories.reactions.ReactionsRepository +import com.nextcloud.talk.threadsoverview.data.ThreadsRepository +import com.nextcloud.talk.ui.PlaybackSpeed +import com.nextcloud.talk.utils.ParticipantPermissions +import com.nextcloud.talk.utils.UserIdUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.preferences.AppPreferences +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import java.io.File +import javax.inject.Inject + +@Suppress("TooManyFunctions", "LongParameterList") +class ChatViewModel @Inject constructor( + // should be removed here. Use it via RetrofitChatNetwork + private val appPreferences: AppPreferences, + private val chatNetworkDataSource: ChatNetworkDataSource, + private val chatRepository: ChatMessageRepository, + private val threadsRepository: ThreadsRepository, + private val conversationRepository: OfflineConversationsRepository, + private val reactionsRepository: ReactionsRepository, + private val mediaRecorderManager: MediaRecorderManager, + private val audioFocusRequestManager: AudioFocusRequestManager, + private val userProvider: CurrentUserProviderNew +) : ViewModel(), + DefaultLifecycleObserver { + + @Inject + lateinit var arbitraryStorageManager: ArbitraryStorageManager + + enum class LifeCycleFlag { + PAUSED, + RESUMED, + STOPPED + } + + private val mediaPlayerManager: MediaPlayerManager = MediaPlayerManager.sharedInstance(appPreferences) + lateinit var currentLifeCycleFlag: LifeCycleFlag + val disposableSet = mutableSetOf() + var mediaPlayerDuration = mediaPlayerManager.mediaPlayerDuration + val mediaPlayerPosition = mediaPlayerManager.mediaPlayerPosition + var chatRoomToken: String = "" + var messageDraft: MessageDraft = MessageDraft() + lateinit var participantPermissions: ParticipantPermissions + + fun getChatRepository(): ChatMessageRepository = chatRepository + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + currentLifeCycleFlag = LifeCycleFlag.RESUMED + mediaRecorderManager.handleOnResume() + chatRepository.handleOnResume() + mediaPlayerManager.handleOnResume() + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + currentLifeCycleFlag = LifeCycleFlag.PAUSED + disposableSet.forEach { disposable -> disposable.dispose() } + disposableSet.clear() + mediaRecorderManager.handleOnPause() + chatRepository.handleOnPause() + mediaPlayerManager.handleOnPause() + + saveMessageDraft() + } + + override fun onStop(owner: LifecycleOwner) { + super.onStop(owner) + currentLifeCycleFlag = LifeCycleFlag.STOPPED + mediaRecorderManager.handleOnStop() + chatRepository.handleOnStop() + mediaPlayerManager.handleOnStop() + } + + val backgroundPlayUIFlow = mediaPlayerManager.backgroundPlayUIFlow + + val mediaPlayerSeekbarObserver: Flow + get() = mediaPlayerManager.mediaPlayerSeekBarPositionMsg + + val managerStateFlow: Flow + get() = mediaPlayerManager.managerState + + val voiceMessagePlayBackUIFlow: Flow + get() = _voiceMessagePlayBackUIFlow + private val _voiceMessagePlayBackUIFlow: MutableSharedFlow = MutableSharedFlow() + + val getAudioFocusChange: LiveData + get() = audioFocusRequestManager.getManagerState + + private val _recordTouchObserver: MutableLiveData = MutableLiveData() + val recordTouchObserver: LiveData + get() = _recordTouchObserver + + private val _getVoiceRecordingInProgress: MutableLiveData = MutableLiveData() + val getVoiceRecordingInProgress: LiveData + get() = _getVoiceRecordingInProgress + + private val _getVoiceRecordingLocked: MutableLiveData = MutableLiveData() + val getVoiceRecordingLocked: LiveData + get() = _getVoiceRecordingLocked + + private val _outOfOfficeViewState = MutableLiveData(OutOfOfficeUIState.None) + val outOfOfficeViewState: LiveData + get() = _outOfOfficeViewState + + private val _unbindRoomResult = MutableLiveData(UnbindRoomUiState.None) + val unbindRoomResult: LiveData + get() = _unbindRoomResult + + private val _voiceMessagePlaybackSpeedPreferences: MutableLiveData> = MutableLiveData() + val voiceMessagePlaybackSpeedPreferences: LiveData> + get() = _voiceMessagePlaybackSpeedPreferences + + private val _getContextChatMessages: MutableLiveData> = MutableLiveData() + val getContextChatMessages: LiveData> + get() = _getContextChatMessages + + private val _threadRetrieveState = MutableStateFlow(ThreadRetrieveUiState.None) + val threadRetrieveState: StateFlow = _threadRetrieveState + + val getOpenGraph: LiveData + get() = _getOpenGraph + private val _getOpenGraph: MutableLiveData = MutableLiveData() + + val getMessageFlow = chatRepository.messageFlow + .onEach { + _chatMessageViewState.value = if (_chatMessageViewState.value == ChatMessageInitialState) { + ChatMessageStartState + } else { + ChatMessageUpdateState + } + }.catch { + _chatMessageViewState.value = ChatMessageErrorState + } + + val getRemoveMessageFlow = chatRepository.removeMessageFlow + + val getUpdateMessageFlow = chatRepository.updateMessageFlow + + val getLastCommonReadFlow = chatRepository.lastCommonReadFlow + + val getLastReadMessageFlow = chatRepository.lastReadMessageFlow + + val getConversationFlow = conversationRepository.conversationFlow + .onEach { + _getRoomViewState.value = GetRoomSuccessState + }.catch { + _getRoomViewState.value = GetRoomErrorState + } + + val getGeneralUIFlow = chatRepository.generalUIFlow + + sealed interface ViewState + + object GetReminderStartState : ViewState + open class GetReminderExistState(val reminder: Reminder) : ViewState + object GetReminderStateSet : ViewState + + private val _getReminderExistState: MutableLiveData = MutableLiveData(GetReminderStartState) + + val getReminderExistState: LiveData + get() = _getReminderExistState + + object GetRoomStartState : ViewState + object GetRoomErrorState : ViewState + object GetRoomSuccessState : ViewState + + private val _getRoomViewState: MutableLiveData = MutableLiveData(GetRoomStartState) + val getRoomViewState: LiveData + get() = _getRoomViewState + + object GetCapabilitiesStartState : ViewState + object GetCapabilitiesErrorState : ViewState + open class GetCapabilitiesInitialLoadState(val spreedCapabilities: SpreedCapability) : ViewState + open class GetCapabilitiesUpdateState(val spreedCapabilities: SpreedCapability) : ViewState + + private val _getCapabilitiesViewState: MutableLiveData = MutableLiveData(GetCapabilitiesStartState) + val getCapabilitiesViewState: LiveData + get() = _getCapabilitiesViewState + + object JoinRoomStartState : ViewState + object JoinRoomErrorState : ViewState + open class JoinRoomSuccessState(val conversationModel: ConversationModel) : ViewState + + private val _joinRoomViewState: MutableLiveData = MutableLiveData(JoinRoomStartState) + val joinRoomViewState: LiveData + get() = _joinRoomViewState + + object LeaveRoomStartState : ViewState + class LeaveRoomSuccessState(val funToCallWhenLeaveSuccessful: (() -> Unit)?) : ViewState + + private val _leaveRoomViewState: MutableLiveData = MutableLiveData(LeaveRoomStartState) + val leaveRoomViewState: LiveData + get() = _leaveRoomViewState + + object ChatMessageInitialState : ViewState + object ChatMessageStartState : ViewState + object ChatMessageUpdateState : ViewState + object ChatMessageErrorState : ViewState + + private val _chatMessageViewState: MutableLiveData = MutableLiveData(ChatMessageInitialState) + val chatMessageViewState: LiveData + get() = _chatMessageViewState + + object DeleteChatMessageStartState : ViewState + class DeleteChatMessageSuccessState(val msg: ChatOverallSingleMessage) : ViewState + object DeleteChatMessageErrorState : ViewState + + private val _deleteChatMessageViewState: MutableLiveData = MutableLiveData(DeleteChatMessageStartState) + val deleteChatMessageViewState: LiveData + get() = _deleteChatMessageViewState + + object CreateRoomStartState : ViewState + object CreateRoomErrorState : ViewState + class CreateRoomSuccessState(val roomOverall: RoomOverall) : ViewState + + private val _createRoomViewState: MutableLiveData = MutableLiveData(CreateRoomStartState) + val createRoomViewState: LiveData + get() = _createRoomViewState + + object ReactionAddedStartState : ViewState + class ReactionAddedSuccessState(val reactionAddedModel: ReactionAddedModel) : ViewState + + private val _reactionAddedViewState: MutableLiveData = MutableLiveData(ReactionAddedStartState) + val reactionAddedViewState: LiveData + get() = _reactionAddedViewState + + object ReactionDeletedStartState : ViewState + class ReactionDeletedSuccessState(val reactionDeletedModel: ReactionDeletedModel) : ViewState + + private val _reactionDeletedViewState: MutableLiveData = MutableLiveData(ReactionDeletedStartState) + val reactionDeletedViewState: LiveData + get() = _reactionDeletedViewState + + fun initData(credentials: String, urlForChatting: String, roomToken: String, threadId: Long?) { + chatRepository.initData(credentials, urlForChatting, roomToken, threadId) + chatRoomToken = roomToken + } + + fun updateConversation(currentConversation: ConversationModel) { + chatRepository.updateConversation(currentConversation) + } + + fun getRoom(token: String) { + _getRoomViewState.value = GetRoomStartState + conversationRepository.getRoom(token) + } + + fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) { + Log.d(TAG, "Remote server ${conversationModel.remoteServer}") + if (conversationModel.remoteServer.isNullOrEmpty()) { + if (_getCapabilitiesViewState.value == GetCapabilitiesStartState) { + _getCapabilitiesViewState.value = GetCapabilitiesInitialLoadState( + user.capabilities!!.spreedCapability!! + ) + } else { + _getCapabilitiesViewState.value = GetCapabilitiesUpdateState(user.capabilities!!.spreedCapability!!) + } + participantPermissions = ParticipantPermissions( + user.capabilities!!.spreedCapability!!, + conversationModel + ) + } else { + chatNetworkDataSource.getCapabilities(user, token) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onNext(spreedCapabilities: SpreedCapability) { + if (_getCapabilitiesViewState.value == GetCapabilitiesStartState) { + _getCapabilitiesViewState.value = GetCapabilitiesInitialLoadState(spreedCapabilities) + } else { + _getCapabilitiesViewState.value = GetCapabilitiesUpdateState(spreedCapabilities) + } + participantPermissions = ParticipantPermissions( + spreedCapabilities, + conversationModel + ) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching spreed capabilities", e) + _getCapabilitiesViewState.value = GetCapabilitiesErrorState + } + + override fun onComplete() { + // unused atm + } + }) + } + } + + fun joinRoom(user: User, token: String, roomPassword: String) { + _joinRoomViewState.value = JoinRoomStartState + chatNetworkDataSource.joinRoom(user, token, roomPassword) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.retry(JOIN_ROOM_RETRY_COUNT) + ?.subscribe(JoinRoomObserver()) + } + + fun setReminder(user: User, roomToken: String, messageId: String, timestamp: Int, chatApiVersion: Int) { + chatNetworkDataSource.setReminder(user, roomToken, messageId, timestamp, chatApiVersion) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(SetReminderObserver()) + } + + fun getReminder(user: User, roomToken: String, messageId: String, chatApiVersion: Int) { + chatNetworkDataSource.getReminder(user, roomToken, messageId, chatApiVersion) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(GetReminderObserver()) + } + + fun overrideReminderState() { + _getReminderExistState.value = GetReminderStateSet + } + + fun deleteReminder(user: User, roomToken: String, messageId: String, chatApiVersion: Int) { + chatNetworkDataSource.deleteReminder(user, roomToken, messageId, chatApiVersion) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onNext(genericOverall: GenericOverall) { + _getReminderExistState.value = GetReminderStartState + } + + override fun onError(e: Throwable) { + Log.d(TAG, "Error when deleting reminder", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + fun leaveRoom(credentials: String, url: String, funToCallWhenLeaveSuccessful: (() -> Unit)?) { + val startNanoTime = System.nanoTime() + chatNetworkDataSource.leaveRoom(credentials, url) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "leaveRoom - leaveRoom - ERROR", e) + } + + override fun onComplete() { + Log.d(TAG, "leaveRoom - leaveRoom - completed: $startNanoTime") + } + + override fun onNext(t: GenericOverall) { + _leaveRoomViewState.value = LeaveRoomSuccessState(funToCallWhenLeaveSuccessful) + _getCapabilitiesViewState.value = GetCapabilitiesStartState + _getRoomViewState.value = GetRoomStartState + } + }) + } + + fun createRoom(credentials: String, url: String, queryMap: Map) { + chatNetworkDataSource.createRoom(credentials, url, queryMap) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onError(e: Throwable) { + _createRoomViewState.value = CreateRoomErrorState + Log.e(TAG, e.message, e) + } + + override fun onComplete() { + // unused atm + } + + override fun onNext(t: RoomOverall) { + _createRoomViewState.value = CreateRoomSuccessState(t) + } + }) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun getThread(credentials: String, url: String) { + viewModelScope.launch { + try { + val thread = threadsRepository.getThread(credentials, url) + _threadRetrieveState.value = ThreadRetrieveUiState.Success(thread.ocs?.data) + } catch (exception: Exception) { + _threadRetrieveState.value = ThreadRetrieveUiState.Error(exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught", "MagicNumber") + fun setThreadNotificationLevel(credentials: String, url: String, level: Int) { + fun updateFollowedThreadsIndicator(notificationLevel: Int?) { + when (notificationLevel) { + 1, 2 -> { + val accountId = UserIdUtils.getIdForUser(userProvider.currentUser.blockingGet()) + arbitraryStorageManager.storeStorageSetting( + accountId, + FOLLOWED_THREADS_EXIST, + true.toString(), + "" + ) + } + } + } + + viewModelScope.launch { + try { + val thread = threadsRepository.setThreadNotificationLevel(credentials, url, level) + updateFollowedThreadsIndicator(thread.ocs?.data?.attendee?.notificationLevel) + _threadRetrieveState.value = ThreadRetrieveUiState.Success(thread.ocs?.data) + } catch (exception: Exception) { + _threadRetrieveState.value = ThreadRetrieveUiState.Error(exception) + } + } + } + + fun loadMessages(withCredentials: String, withUrl: String) { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl) + bundle.putString(BundleKeys.KEY_CREDENTIALS, withCredentials) + chatRepository.initScopeAndLoadInitialMessages( + withNetworkParams = bundle + ) + } + + fun loadMoreMessages( + beforeMessageId: Long, + roomToken: String, + withMessageLimit: Int, + withCredentials: String, + withUrl: String + ) { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl) + bundle.putString(BundleKeys.KEY_CREDENTIALS, withCredentials) + chatRepository.loadMoreMessages( + beforeMessageId, + roomToken, + withMessageLimit, + withNetworkParams = bundle + ) + } + + // fun initMessagePolling(withCredentials: String, withUrl: String, roomToken: String) { + // val bundle = Bundle() + // bundle.putString(BundleKeys.KEY_CHAT_URL, withUrl) + // bundle.putString(BundleKeys.KEY_CREDENTIALS, withCredentials) + // chatRepository.initMessagePolling(roomToken, withNetworkParams = bundle) + // } + + fun deleteChatMessages(credentials: String, url: String, messageId: String) { + chatNetworkDataSource.deleteChatMessage(credentials, url) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onError(e: Throwable) { + Log.e( + TAG, + "Something went wrong when trying to delete message with id " + + messageId, + e + ) + _deleteChatMessageViewState.value = DeleteChatMessageErrorState + } + + override fun onComplete() { + // unused atm + } + + override fun onNext(t: ChatOverallSingleMessage) { + _deleteChatMessageViewState.value = DeleteChatMessageSuccessState(t) + } + }) + } + + fun setChatReadMarker(credentials: String, url: String, previousMessageId: Int) { + chatNetworkDataSource.setChatReadMarker(credentials, url, previousMessageId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onError(e: Throwable) { + Log.e(TAG, e.message, e) + } + + override fun onComplete() { + // unused atm + } + + override fun onNext(t: GenericOverall) { + // unused atm + } + }) + } + + fun shareToNotes(credentials: String, url: String, message: String, displayName: String) { + chatNetworkDataSource.shareToNotes(credentials, url, message, displayName) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onNext(genericOverall: ChatOverallSingleMessage) { + // unused atm + } + + override fun onError(e: Throwable) { + Log.d(TAG, "Error when sharing to notes $e") + } + + override fun onComplete() { + // unused atm + } + }) + } + + suspend fun checkForNoteToSelf(credentials: String, baseUrl: String): ConversationModel? { + val response = chatNetworkDataSource.checkForNoteToSelf(credentials, baseUrl) + if (response.ocs?.meta?.statusCode == HTTP_CODE_OK) { + val noteToSelfConversation = ConversationModel.mapToConversationModel( + response.ocs?.data!!, + userProvider.currentUser.blockingGet() + ) + return noteToSelfConversation + } else { + return null + } + } + + fun shareLocationToNotes(credentials: String, url: String, objectType: String, objectId: String, metadata: String) { + chatNetworkDataSource.shareLocationToNotes(credentials, url, objectType, objectId, metadata) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onNext(genericOverall: GenericOverall) { + // unused atm + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when sharing location to notes $e") + } + + override fun onComplete() { + // unused atm + } + }) + } + + fun deleteReaction(roomToken: String, chatMessage: ChatMessage, emoji: String) { + reactionsRepository.deleteReaction(roomToken, chatMessage, emoji) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onError(e: Throwable) { + Log.d(TAG, "$e") + } + + override fun onComplete() { + // unused atm + } + + override fun onNext(reactionDeletedModel: ReactionDeletedModel) { + if (reactionDeletedModel.success) { + _reactionDeletedViewState.value = ReactionDeletedSuccessState(reactionDeletedModel) + } + } + }) + } + + fun addReaction(roomToken: String, chatMessage: ChatMessage, emoji: String) { + reactionsRepository.addReaction(roomToken, chatMessage, emoji) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onError(e: Throwable) { + Log.d(TAG, "$e") + } + + override fun onComplete() { + // unused atm + } + + override fun onNext(reactionAddedModel: ReactionAddedModel) { + if (reactionAddedModel.success) { + _reactionAddedViewState.value = ReactionAddedSuccessState(reactionAddedModel) + } + } + }) + } + + fun startAudioRecording(context: Context, currentConversation: ConversationModel) { + audioFocusRequestManager.audioFocusRequest(true) { + Log.d(TAG, "Recording Started") + mediaRecorderManager.start(context, currentConversation) + _getVoiceRecordingInProgress.postValue(true) + } + } + + fun stopAudioRecording() { + audioFocusRequestManager.audioFocusRequest(false) { + mediaRecorderManager.stop() + _getVoiceRecordingInProgress.postValue(false) + Log.d(TAG, "Recording stopped") + } + } + + fun stopAndSendAudioRecording(roomToken: String = "", replyToMessageId: Int? = null, displayName: String) { + stopAudioRecording() + + if (mediaRecorderManager.mediaRecorderState != MediaRecorderManager.MediaRecorderState.ERROR) { + val uri = Uri.fromFile(File(mediaRecorderManager.currentVoiceRecordFile)) + Log.d(TAG, "File uploaded") + uploadFile( + fileUri = uri.toString(), + isVoiceMessage = true, + caption = "", + roomToken = roomToken, + replyToMessageId = replyToMessageId, + displayName = displayName + ) + } + } + + fun stopAndDiscardAudioRecording() { + stopAudioRecording() + Log.d(TAG, "File discarded") + val cachedFile = File(mediaRecorderManager.currentVoiceRecordFile) + cachedFile.delete() + } + + fun getCurrentVoiceRecordFile(): String = mediaRecorderManager.currentVoiceRecordFile + + fun uploadFile( + fileUri: String, + isVoiceMessage: Boolean, + caption: String = "", + roomToken: String = "", + replyToMessageId: Int? = null, + displayName: String + ) { + val metaDataMap = mutableMapOf() + var room = "" + + if (!participantPermissions.hasChatPermission()) { + Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions") + return + } + + if (replyToMessageId != 0) { + metaDataMap["replyTo"] = replyToMessageId.toString() + } + + if (isVoiceMessage) { + metaDataMap["messageType"] = "voice-message" + } + + if (caption != "") { + metaDataMap["caption"] = caption + } + + val metaData = Gson().toJson(metaDataMap) + + room = if (roomToken == "") chatRoomToken else roomToken + + try { + require(fileUri.isNotEmpty()) + UploadAndShareFilesWorker.upload( + fileUri, + room, + displayName, + metaData + ) + } catch (e: IllegalArgumentException) { + Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e) + } + } + + fun postToRecordTouchObserver(float: Float) { + _recordTouchObserver.postValue(float) + } + + fun setVoiceRecordingLocked(boolean: Boolean) { + _getVoiceRecordingLocked.postValue(boolean) + } + + // Made this so that the MediaPlayer in ChatActivity can be focused. Eventually the player logic should be moved + // to the MediaPlayerManager class, so the audio focus logic can be handled in ChatViewModel, as it's done in + // the MessageInputViewModel + fun audioRequest(request: Boolean, callback: () -> Unit) { + audioFocusRequestManager.audioFocusRequest(request, callback) + } + + fun handleOrientationChange() { + _getCapabilitiesViewState.value = GetCapabilitiesStartState + } + + fun getMessageById(url: String, conversationModel: ConversationModel, messageId: Long): Flow = + flow { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_CHAT_URL, url) + bundle.putString( + BundleKeys.KEY_CREDENTIALS, + userProvider.currentUser.blockingGet().getCredentials() + ) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversationModel.token) + + val message = chatRepository.getMessage(messageId, bundle) + emit(message.first()) + } + + suspend fun getNumberOfThreadReplies(threadId: Long): Int = chatRepository.getNumberOfThreadReplies(threadId) + + fun setPlayBack(speed: PlaybackSpeed) { + mediaPlayerManager.setPlayBackSpeed(speed) + CoroutineScope(Dispatchers.Default).launch { + _voiceMessagePlayBackUIFlow.emit(speed) + } + } + + fun startMediaPlayer(path: String) { + audioRequest(true) { + mediaPlayerManager.start(path) + } + } + + fun startCyclingMediaPlayer() = audioRequest(true, mediaPlayerManager::startCycling) + + fun pauseMediaPlayer(notifyUI: Boolean) { + audioRequest(false) { + mediaPlayerManager.pause(notifyUI) + } + } + + fun seekToMediaPlayer(progress: Int) = mediaPlayerManager.seekTo(progress) + + fun stopMediaPlayer() = audioRequest(false, mediaPlayerManager::stop) + + fun queueInMediaPlayer(path: String, msg: ChatMessage) = mediaPlayerManager.addToPlayList(path, msg) + + fun clearMediaPlayerQueue() = mediaPlayerManager.clearPlayList() + + inner class JoinRoomObserver : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onNext(conversationModel: ConversationModel) { + _joinRoomViewState.value = JoinRoomSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when joining room") + _joinRoomViewState.value = JoinRoomErrorState + } + + override fun onComplete() { + // unused atm + } + } + + inner class SetReminderObserver : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onNext(reminder: Reminder) { + Log.d(TAG, "reminder set successfully") + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when sending reminder, $e") + } + + override fun onComplete() { + // unused atm + } + } + + inner class GetReminderObserver : Observer { + override fun onSubscribe(d: Disposable) { + disposableSet.add(d) + } + + override fun onNext(reminder: Reminder) { + _getReminderExistState.value = GetReminderExistState(reminder) + } + + override fun onError(e: Throwable) { + Log.d(TAG, "Error when getting reminder $e") + _getReminderExistState.value = GetReminderStartState + } + + override fun onComplete() { + // unused atm + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun outOfOfficeStatusOfUser(credentials: String, baseUrl: String, userId: String) { + viewModelScope.launch { + try { + val response = chatNetworkDataSource.getOutOfOfficeStatusForUser(credentials, baseUrl, userId) + _outOfOfficeViewState.value = OutOfOfficeUIState.Success(response.ocs?.data!!) + } catch (exception: Exception) { + _outOfOfficeViewState.value = OutOfOfficeUIState.Error(exception) + } + } + } + + fun deleteTempMessage(chatMessage: ChatMessage) { + viewModelScope.launch { + chatRepository.deleteTempMessage(chatMessage) + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun unbindRoom(credentials: String, baseUrl: String, roomToken: String) { + viewModelScope.launch { + try { + val response = chatNetworkDataSource.unbindRoom(credentials, baseUrl, roomToken) + _unbindRoomResult.value = UnbindRoomUiState.Success(response.ocs?.meta?.statusCode!!) + } catch (exception: Exception) { + _unbindRoomResult.value = UnbindRoomUiState.Error(exception.message.toString()) + } + } + } + + fun resendMessage(credentials: String, urlForChat: String, message: ChatMessage) { + viewModelScope.launch { + chatRepository.resendChatMessage( + credentials, + urlForChat, + message.message.orEmpty(), + message.actorDisplayName.orEmpty(), + message.parentMessageId?.toIntOrZero() ?: 0, + false, + message.referenceId.orEmpty() + ).collect { result -> + if (result.isSuccess) { + Log.d(TAG, "resend successful") + } else { + Log.e(TAG, "resend failed") + } + } + } + } + + fun getContextForChatMessages(credentials: String, baseUrl: String, token: String, messageId: String, limit: Int) { + viewModelScope.launch { + val messages = chatNetworkDataSource.getContextForChatMessage( + credentials, + baseUrl, + token, + messageId, + limit + ) + + _getContextChatMessages.value = messages + } + } + + fun getOpenGraph(credentials: String, baseUrl: String, urlToPreview: String) { + viewModelScope.launch { + _getOpenGraph.value = chatNetworkDataSource.getOpenGraph(credentials, baseUrl, urlToPreview) + } + } + + suspend fun updateMessageDraft() { + val model = conversationRepository.getLocallyStoredConversation(chatRoomToken) + model?.messageDraft?.let { + messageDraft = it + } + } + + fun saveMessageDraft() { + CoroutineScope(Dispatchers.IO).launch { + val model = conversationRepository.getLocallyStoredConversation(chatRoomToken) + model?.let { + it.messageDraft = messageDraft + conversationRepository.updateConversation(it) + } + } + } + + fun clearThreadTitle() { + messageDraft.threadTitle = "" + saveMessageDraft() + } + + companion object { + private val TAG = ChatViewModel::class.simpleName + const val JOIN_ROOM_RETRY_COUNT: Long = 3 + const val HTTP_CODE_OK: Int = 200 + } + + sealed class OutOfOfficeUIState { + data object None : OutOfOfficeUIState() + data class Success(val userAbsence: UserAbsenceData) : OutOfOfficeUIState() + data class Error(val exception: Exception) : OutOfOfficeUIState() + } + + sealed class UnbindRoomUiState { + data object None : UnbindRoomUiState() + data class Success(val statusCode: Int) : UnbindRoomUiState() + data class Error(val message: String) : UnbindRoomUiState() + } + + sealed class ThreadRetrieveUiState { + data object None : ThreadRetrieveUiState() + data class Success(val thread: ThreadInfo?) : ThreadRetrieveUiState() + data class Error(val exception: Exception) : ThreadRetrieveUiState() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt new file mode 100644 index 0000000..bc7e609 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/MessageInputViewModel.kt @@ -0,0 +1,280 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus juliuslinus1@gmail.com + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.chat.viewmodels + +import android.content.Context +import android.util.Log +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.chat.data.ChatMessageRepository +import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager +import com.nextcloud.talk.chat.data.io.AudioRecorderManager +import com.nextcloud.talk.chat.data.io.MediaPlayerManager +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage +import com.nextcloud.talk.utils.message.SendMessageUtils +import com.stfalcon.chatkit.commons.models.IMessage +import io.reactivex.disposables.Disposable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("Detekt.TooManyFunctions") +class MessageInputViewModel @Inject constructor( + private val audioRecorderManager: AudioRecorderManager, + private val mediaPlayerManager: MediaPlayerManager, + private val audioFocusRequestManager: AudioFocusRequestManager +) : ViewModel(), + DefaultLifecycleObserver { + + enum class LifeCycleFlag { + PAUSED, + RESUMED, + STOPPED + } + + lateinit var chatRepository: ChatMessageRepository + lateinit var currentLifeCycleFlag: LifeCycleFlag + val disposableSet = mutableSetOf() + + fun setData(chatMessageRepository: ChatMessageRepository) { + chatRepository = chatMessageRepository + } + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + currentLifeCycleFlag = LifeCycleFlag.RESUMED + audioRecorderManager.handleOnResume() + mediaPlayerManager.handleOnResume() + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + currentLifeCycleFlag = LifeCycleFlag.PAUSED + disposableSet.forEach { disposable -> disposable.dispose() } + disposableSet.clear() + audioRecorderManager.handleOnPause() + mediaPlayerManager.handleOnPause() + } + + override fun onStop(owner: LifecycleOwner) { + super.onStop(owner) + currentLifeCycleFlag = LifeCycleFlag.STOPPED + audioRecorderManager.handleOnStop() + mediaPlayerManager.handleOnStop() + } + + val getAudioFocusChange: LiveData + get() = audioFocusRequestManager.getManagerState + + private val _getRecordingTime: MutableLiveData = MutableLiveData(0L) + val getRecordingTime: LiveData + get() = _getRecordingTime + + val micInputAudioObserver: LiveData> + get() = audioRecorderManager.getAudioValues + + val mediaPlayerSeekbarObserver: Flow + get() = mediaPlayerManager.mediaPlayerSeekBarPosition + + private val _getEditChatMessage: MutableLiveData = MutableLiveData() + val getEditChatMessage: LiveData + get() = _getEditChatMessage + + private val _getReplyChatMessage: MutableLiveData = MutableLiveData() + val getReplyChatMessage: LiveData + get() = _getReplyChatMessage + + object CreateThreadStartState : ViewState + class CreateThreadEditState : ViewState + + private val _createThreadViewState: MutableLiveData = MutableLiveData(CreateThreadStartState) + val createThreadViewState: LiveData + get() = _createThreadViewState + + sealed interface ViewState + + object SendChatMessageStartState : ViewState + class SendChatMessageSuccessState(val message: CharSequence) : ViewState + class SendChatMessageErrorState(val message: CharSequence) : ViewState + + private val _sendChatMessageViewState: MutableLiveData = MutableLiveData(SendChatMessageStartState) + val sendChatMessageViewState: LiveData + get() = _sendChatMessageViewState + + object EditMessageErrorState : ViewState + class EditMessageSuccessState(val messageEdited: ChatOverallSingleMessage) : ViewState + + private val _editMessageViewState: MutableLiveData = MutableLiveData() + val editMessageViewState: LiveData + get() = _editMessageViewState + + private val _isVoicePreviewPlaying: MutableLiveData = MutableLiveData(false) + val isVoicePreviewPlaying: LiveData + get() = _isVoicePreviewPlaying + + private val _callStartedFlow: MutableLiveData> = MutableLiveData() + val callStartedFlow: LiveData> + get() = _callStartedFlow + + @Suppress("LongParameterList") + fun sendChatMessage( + credentials: String, + url: String, + message: String, + displayName: String, + replyTo: Int, + sendWithoutNotification: Boolean, + threadTitle: String? + ) { + val referenceId = SendMessageUtils().generateReferenceId() + Log.d(TAG, "Random SHA-256 Hash: $referenceId") + + viewModelScope.launch { + chatRepository.addTemporaryMessage( + message, + displayName, + replyTo, + sendWithoutNotification, + referenceId + ).collect { result -> + if (result.isSuccess) { + Log.d(TAG, "temp message ref id: " + (result.getOrNull()?.referenceId ?: "none")) + + _sendChatMessageViewState.value = SendChatMessageSuccessState(message) + } else { + _sendChatMessageViewState.value = SendChatMessageErrorState(message) + } + } + } + + viewModelScope.launch { + chatRepository.sendChatMessage( + credentials, + url, + message, + displayName, + replyTo, + sendWithoutNotification, + referenceId, + threadTitle + ).collect { result -> + if (result.isSuccess) { + Log.d(TAG, "received ref id: " + (result.getOrNull()?.referenceId ?: "none")) + + _sendChatMessageViewState.value = SendChatMessageSuccessState(message) + } else { + _sendChatMessageViewState.value = SendChatMessageErrorState(message) + } + } + } + } + + fun sendUnsentMessages(credentials: String, url: String) { + viewModelScope.launch { + chatRepository.sendUnsentChatMessages( + credentials, + url + ) + } + } + + fun editChatMessage(credentials: String, url: String, text: String) { + viewModelScope.launch { + chatRepository.editChatMessage( + credentials, + url, + text + ).collect { result -> + if (result.isSuccess) { + _editMessageViewState.value = EditMessageSuccessState(result.getOrNull()!!) + } else { + _editMessageViewState.value = EditMessageErrorState + } + } + } + } + + fun editTempChatMessage(message: ChatMessage, editedMessageText: String) { + viewModelScope.launch { + chatRepository.editTempChatMessage( + message, + editedMessageText + ).collect {} + } + } + + fun reply(message: ChatMessage?) { + _getReplyChatMessage.postValue(message) + } + + fun edit(message: IMessage?) { + _getEditChatMessage.postValue(message) + } + + fun startMicInput(context: Context) { + audioFocusRequestManager.audioFocusRequest(true) { + audioRecorderManager.start(context) + } + } + + fun stopMicInput() { + audioFocusRequestManager.audioFocusRequest(false) { + audioRecorderManager.stop() + } + } + + fun startMediaPlayer(path: String) { + audioFocusRequestManager.audioFocusRequest(true) { + mediaPlayerManager.start(path) + _isVoicePreviewPlaying.postValue(true) + } + } + + fun pauseMediaPlayer() { + audioFocusRequestManager.audioFocusRequest(false) { + mediaPlayerManager.pause(false) + _isVoicePreviewPlaying.postValue(false) + } + } + + fun stopMediaPlayer() { + audioFocusRequestManager.audioFocusRequest(false) { + mediaPlayerManager.stop() + _isVoicePreviewPlaying.postValue(false) + } + } + + fun seekMediaPlayerTo(progress: Int) { + mediaPlayerManager.seekTo(progress) + } + + fun setRecordingTime(time: Long) { + _getRecordingTime.postValue(time) + } + + fun showCallStartedIndicator(recent: ChatMessage, show: Boolean) { + _callStartedFlow.postValue(Pair(recent, show)) + } + + fun startThreadCreation() { + _createThreadViewState.postValue(CreateThreadEditState()) + } + + fun stopThreadCreation() { + _createThreadViewState.postValue(CreateThreadStartState) + } + + companion object { + private val TAG = MessageInputViewModel::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/components/ColoredStatusBar.kt b/app/src/main/java/com/nextcloud/talk/components/ColoredStatusBar.kt new file mode 100644 index 0000000..60613a4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/components/ColoredStatusBar.kt @@ -0,0 +1,60 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.components + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.windowInsetsTopHeight +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +@Composable +fun ColoredStatusBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + Box(modifier = Modifier.fillMaxSize()) { + Box( + Modifier + .windowInsetsTopHeight(WindowInsets.statusBars) + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + ) + } + } else { + ColorLegacyStatusBar() + } +} + +@Composable +private fun ColorLegacyStatusBar() { + val view = LocalView.current + val isDarkMode = isSystemInDarkTheme() + val statusBarColor = MaterialTheme.colorScheme.surface.toArgb() + + DisposableEffect(isDarkMode) { + val activity = view.context as Activity + activity.window.statusBarColor = statusBarColor + + WindowCompat.getInsetsController(activity.window, activity.window.decorView).apply { + isAppearanceLightStatusBars = !isDarkMode + isAppearanceLightNavigationBars = !isDarkMode + } + onDispose { } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/components/StandardAppBar.kt b/app/src/main/java/com/nextcloud/talk/components/StandardAppBar.kt new file mode 100644 index 0000000..fefe97b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/components/StandardAppBar.kt @@ -0,0 +1,88 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.components + +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.nextcloud.talk.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StandardAppBar(title: String, menuItems: List Unit>>?) { + val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + + var expanded by remember { mutableStateOf(false) } + + TopAppBar( + title = { Text(text = title) }, + navigationIcon = { + IconButton( + onClick = { backDispatcher?.onBackPressed() } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back_button) + ) + } + }, + actions = { + if (!menuItems.isNullOrEmpty()) { + Box { + IconButton(onClick = { expanded = true }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(R.string.nc_common_more_options) + ) + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.background(color = colorResource(id = R.color.bg_default)) + ) { + menuItems?.forEach { (label, action) -> + DropdownMenuItem( + text = { Text(label) }, + onClick = { + action() + expanded = false + } + ) + } + } + } + } + } + ) +} + +@Preview(showBackground = true) +@Composable +fun AppBarPreview() { + StandardAppBar("title", null) +} diff --git a/app/src/main/java/com/nextcloud/talk/components/VerticallyCenteredRow.kt b/app/src/main/java/com/nextcloud/talk/components/VerticallyCenteredRow.kt new file mode 100644 index 0000000..71d5a08 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/components/VerticallyCenteredRow.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun VerticallyCenteredRow(content: @Composable RowScope.() -> Unit) { + Row( + modifier = Modifier.fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically, + content = content + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt new file mode 100644 index 0000000..5f93ddf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsActivity.kt @@ -0,0 +1,83 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import autodagger.AutoInjector +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS +import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.utils.bundle.BundleKeys +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ContactsActivity : BaseActivity() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var contactsViewModel: ContactsViewModel + + @SuppressLint("UnrememberedMutableState") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + contactsViewModel = ViewModelProvider(this, viewModelFactory)[ContactsViewModel::class.java] + setContent { + val isAddParticipants = intent.getBooleanExtra(BundleKeys.KEY_ADD_PARTICIPANTS, false) + val hideAlreadyAddedParticipants = intent.getBooleanExtra(KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS, false) + contactsViewModel.updateIsAddParticipants(isAddParticipants) + contactsViewModel.hideAlreadyAddedParticipants(hideAlreadyAddedParticipants) + if (isAddParticipants) { + contactsViewModel.updateShareTypes( + listOf( + ShareType.Group.shareType, + ShareType.Email.shareType, + ShareType.Circle.shareType + ) + ) + contactsViewModel.getContactsFromSearchParams() + } + val colorScheme = viewThemeUtils.getColorScheme(this) + val uiState = contactsViewModel.contactsViewState.collectAsStateWithLifecycle() + + val selectedParticipants = remember { + intent?.getParcelableArrayListExtraProvider("selectedParticipants") + ?: emptyList() + }.toSet().toMutableList() + contactsViewModel.updateSelectedParticipants(selectedParticipants) + + MaterialTheme( + colorScheme = colorScheme + ) { + ColoredStatusBar() + ContactsScreen( + contactsViewModel = contactsViewModel, + uiState = uiState.value + ) + } + } + } +} + +class CompanionClass { + companion object { + internal val TAG = ContactsActivity::class.simpleName + internal const val ROOM_TYPE_ONE_ONE = "1" + const val KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS: String = "KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsApplication.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsApplication.kt new file mode 100644 index 0000000..445f092 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsApplication.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import android.app.Application +import coil.ImageLoader +import coil.ImageLoaderFactory +import coil.disk.DiskCache +import coil.memory.MemoryCache +import coil.util.DebugLogger +import com.nextcloud.talk.utils.ContactUtils + +class ContactsApplication : + Application(), + ImageLoaderFactory { + override fun newImageLoader(): ImageLoader { + val imageLoader = ImageLoader.Builder(this) + .memoryCache { + MemoryCache.Builder(this) + .maxSizePercent(ContactUtils.CACHE_MEMORY_SIZE_PERCENTAGE) + .build() + } + .diskCache { + DiskCache.Builder() + .maxSizePercent(ContactUtils.CACHE_DISK_SIZE_PERCENTAGE) + .directory(cacheDir) + .build() + } + .logger(DebugLogger()) + .build() + return imageLoader + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt new file mode 100644 index 0000000..03551eb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepository.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.conversations.RoomOverall + +interface ContactsRepository { + suspend fun getContacts(searchQuery: String?, shareTypes: List): AutocompleteOverall + suspend fun createRoom( + roomType: String, + sourceType: String?, + userId: String, + conversationName: String? + ): RoomOverall + fun getImageUri(avatarId: String, requestBigSize: Boolean): String +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt new file mode 100644 index 0000000..1e51c68 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsRepositoryImpl.kt @@ -0,0 +1,99 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import android.util.Log +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.RetrofitBucket +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ContactUtils +import com.nextcloud.talk.utils.NoSupportedApiException +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import javax.inject.Inject + +class ContactsRepositoryImpl @Inject constructor( + private val ncApiCoroutines: NcApiCoroutines, + currentUserProvider: CurrentUserProviderNew +) : ContactsRepository { + + private val _currentUser = currentUserProvider.currentUser.blockingGet() + val currentUser: User = _currentUser + val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) + + override suspend fun getContacts(searchQuery: String?, shareTypes: List): AutocompleteOverall { + val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForContactsSearchFor14( + currentUser.baseUrl!!, + searchQuery + ) + + val modifiedQueryMap: HashMap = HashMap(retrofitBucket.queryMap) + modifiedQueryMap["limit"] = ContactUtils.MAX_CONTACT_LIMIT + modifiedQueryMap["shareTypes[]"] = shareTypes + val response = ncApiCoroutines.getContactsWithSearchParam( + credentials, + retrofitBucket.url, + shareTypes, + modifiedQueryMap + ) + return response + } + + override suspend fun createRoom( + roomType: String, + sourceType: String?, + userId: String, + conversationName: String? + ): RoomOverall { + val apiVersion = + try { + ApiUtils.getConversationApiVersion(_currentUser, intArrayOf(ApiUtils.API_V4, 1)) + } catch (e: NoSupportedApiException) { + // There were crash reports for: + // Exception java.lang.RuntimeException: + // ... + // Caused by com.nextcloud.talk.utils.NoSupportedApiException: + // at com.nextcloud.talk.utils.ApiUtils.getConversationApiVersion (ApiUtils.kt:134) + // at com.nextcloud.talk.contacts.ContactsRepositoryImpl. (ContactsRepositoryImpl.kt:28) + // + // This could happen because of missing capabilities for user and should be fixed. + // As a fallback, API v4 is guessed + + Log.e(TAG, "Failed to get an Api version for conversation.", e) + ApiUtils.API_V4 + } + + val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + version = apiVersion, + baseUrl = _currentUser.baseUrl, + roomType = roomType, + source = sourceType, + invite = userId, + conversationName = conversationName + ) + val response = ncApiCoroutines.createRoom( + credentials, + retrofitBucket.url, + retrofitBucket.queryMap + ) + return response + } + + override fun getImageUri(avatarId: String, requestBigSize: Boolean): String = + ApiUtils.getUrlForAvatar( + _currentUser.baseUrl, + avatarId, + requestBigSize + ) + + companion object { + private val TAG = ContactsRepositoryImpl::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt new file mode 100644 index 0000000..478e0f0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsScreen.kt @@ -0,0 +1,83 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nextcloud.talk.R +import com.nextcloud.talk.contacts.components.ContactsAppBar +import com.nextcloud.talk.contacts.components.ContactsList +import com.nextcloud.talk.contacts.components.ContactsSearchAppBar +import com.nextcloud.talk.contacts.components.ConversationCreationOptions + +@Composable +fun ContactsScreen(contactsViewModel: ContactsViewModel, uiState: ContactsUiState) { + val searchQuery by contactsViewModel.searchQuery.collectAsStateWithLifecycle() + val isSearchActive by contactsViewModel.isSearchActive.collectAsStateWithLifecycle() + val isAddParticipants by contactsViewModel.isAddParticipantsView.collectAsStateWithLifecycle() + val autocompleteUsers by contactsViewModel.selectedParticipantsList.collectAsStateWithLifecycle() + val enableAddButton by contactsViewModel.enableAddButton.collectAsStateWithLifecycle() + + Scaffold( + modifier = Modifier + .statusBarsPadding() + .displayCutoutPadding(), + topBar = { + if (isSearchActive) { + ContactsSearchAppBar( + searchQuery = searchQuery, + onTextChange = { + contactsViewModel.updateSearchQuery(it) + contactsViewModel.getContactsFromSearchParams() + }, + onCloseSearch = { + contactsViewModel.updateSearchQuery("") + contactsViewModel.setSearchActive(false) + contactsViewModel.getContactsFromSearchParams() + }, + enableAddButton = enableAddButton, + isAddParticipants = isAddParticipants, + clickAddButton = { contactsViewModel.modifyClickAddButton(true) } + ) + } else { + ContactsAppBar( + isAddParticipants = isAddParticipants, + autocompleteUsers = autocompleteUsers, + onStartSearch = { contactsViewModel.setSearchActive(true) } + ) + } + }, + content = { paddingValues -> + Column( + Modifier + .background(colorResource(id = R.color.bg_default)) + .padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, paddingValues.calculateBottomPadding()) + ) { + if (!isAddParticipants) { + ConversationCreationOptions() + } + + ContactsList( + contactsUiState = uiState, + contactsViewModel = contactsViewModel + ) + } + } + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt b/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt new file mode 100644 index 0000000..ed0e4b6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ContactsViewModel.kt @@ -0,0 +1,157 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.models.json.conversations.Conversation +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ContactsViewModel @Inject constructor(private val repository: ContactsRepository) : ViewModel() { + + private val _contactsViewState = MutableStateFlow(ContactsUiState.None) + val contactsViewState: StateFlow = _contactsViewState + private val _roomViewState = MutableStateFlow(RoomUiState.None) + val roomViewState: StateFlow = _roomViewState + private val _searchQuery = MutableStateFlow("") + val searchQuery: StateFlow = _searchQuery + private val shareTypes: MutableList = mutableListOf(ShareType.User.shareType) + val shareTypeList: List = shareTypes + private val _isSearchActive = MutableStateFlow(false) + val isSearchActive: StateFlow = _isSearchActive + private val selectedParticipants = MutableStateFlow>(emptyList()) + val selectedParticipantsList: StateFlow> = selectedParticipants.asStateFlow() + private val _isAddParticipantsView = MutableStateFlow(false) + val isAddParticipantsView: StateFlow = _isAddParticipantsView + + private val _enableAddButton = MutableStateFlow(false) + val enableAddButton: StateFlow = _enableAddButton + + @Suppress("PropertyName") + private val _selectedContacts = MutableStateFlow>(emptyList()) + + @Suppress("PropertyName") + private val _clickAddButton = MutableStateFlow(false) + + private var hideAlreadyAddedParticipants: Boolean = false + + init { + getContactsFromSearchParams() + } + + fun updateSearchQuery(query: String) { + _searchQuery.value = query + } + + fun modifyClickAddButton(value: Boolean) { + _clickAddButton.value = value + } + + fun selectContact(contact: AutocompleteUser) { + val updatedParticipants = selectedParticipants.value + contact + selectedParticipants.value = updatedParticipants + _selectedContacts.value = _selectedContacts.value + contact + } + + fun updateAddButtonState() { + if (_selectedContacts.value.isEmpty()) { + _enableAddButton.value = false + } else { + _enableAddButton.value = true + } + } + + fun deselectContact(contact: AutocompleteUser) { + val updatedParticipants = selectedParticipants.value - contact + selectedParticipants.value = updatedParticipants + _selectedContacts.value = _selectedContacts.value - contact + } + + fun updateSelectedParticipants(participants: List) { + selectedParticipants.value = participants + } + fun setSearchActive(searchState: Boolean) { + _isSearchActive.value = searchState + } + + fun updateShareTypes(value: List) { + shareTypes.addAll(value) + } + + fun updateIsAddParticipants(value: Boolean) { + _isAddParticipantsView.value = value + } + + fun hideAlreadyAddedParticipants(value: Boolean) { + hideAlreadyAddedParticipants = value + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun getContactsFromSearchParams(query: String = "") { + _contactsViewState.value = ContactsUiState.Loading + viewModelScope.launch { + try { + val contacts = repository.getContacts( + if (query != "") query else searchQuery.value, + shareTypeList + ) + val contactsList: MutableList? = contacts.ocs!!.data?.toMutableList() + + if (hideAlreadyAddedParticipants && !_clickAddButton.value) { + contactsList?.removeAll(selectedParticipants.value) + } + if (_clickAddButton.value) { + contactsList?.removeAll(selectedParticipants.value) + contactsList?.addAll(_selectedContacts.value) + } + _contactsViewState.value = ContactsUiState.Success(contactsList) + } catch (exception: Exception) { + _contactsViewState.value = ContactsUiState.Error(exception.message ?: "") + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun createRoom(roomType: String, sourceType: String?, userId: String, conversationName: String?) { + viewModelScope.launch { + try { + val room = repository.createRoom( + roomType, + sourceType, + userId, + conversationName + ) + + val conversation: Conversation? = room.ocs?.data + _roomViewState.value = RoomUiState.Success(conversation) + } catch (exception: Exception) { + _roomViewState.value = RoomUiState.Error(exception.message ?: "") + } + } + } + fun getImageUri(avatarId: String, requestBigSize: Boolean): String = + repository.getImageUri(avatarId, requestBigSize) +} + +sealed class ContactsUiState { + data object None : ContactsUiState() + data object Loading : ContactsUiState() + data class Success(val contacts: List?) : ContactsUiState() + data class Error(val message: String) : ContactsUiState() +} + +sealed class RoomUiState { + data object None : RoomUiState() + data class Success(val conversation: Conversation?) : RoomUiState() + data class Error(val message: String) : RoomUiState() +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ImageRequest.kt b/app/src/main/java/com/nextcloud/talk/contacts/ImageRequest.kt new file mode 100644 index 0000000..6d8eda4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ImageRequest.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import android.content.Context +import androidx.compose.runtime.Composable +import coil.request.ImageRequest +import coil.size.Size +import coil.transform.CircleCropTransformation +import coil.transform.RoundedCornersTransformation + +@Composable +fun loadImage(imageUri: String?, context: Context, errorPlaceholderImage: Int): ImageRequest { + val imageRequest = ImageRequest.Builder(context) + .data(imageUri) + .transformations(CircleCropTransformation()) + .error(errorPlaceholderImage) + .placeholder(errorPlaceholderImage) + .build() + return imageRequest +} + +@Composable +fun load(imageUri: String?, context: Context, errorPlaceholderImage: Int): ImageRequest { + val imageRequest = ImageRequest.Builder(context) + .data(imageUri) + .size(Size.ORIGINAL) + .transformations(RoundedCornersTransformation()) + .error(errorPlaceholderImage) + .placeholder(errorPlaceholderImage) + .build() + return imageRequest +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt b/app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt new file mode 100644 index 0000000..39fad32 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/ShareType.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +enum class ShareType(val shareType: String) { + User("0"), + Group("1"), + Email("4"), + Remote("5"), + Circle("7") +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt new file mode 100644 index 0000000..05cd843 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactItemRow.kt @@ -0,0 +1,120 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.nextcloud.talk.R +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.contacts.CompanionClass +import com.nextcloud.talk.contacts.ContactsViewModel +import com.nextcloud.talk.contacts.RoomUiState +import com.nextcloud.talk.contacts.loadImage +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.utils.bundle.BundleKeys + +@Composable +fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewModel, context: Context) { + var isSelected by remember { mutableStateOf(contactsViewModel.selectedParticipantsList.value.contains(contact)) } + val roomUiState by contactsViewModel.roomViewState.collectAsState() + val isAddParticipants = contactsViewModel.isAddParticipantsView.collectAsState() + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = { + if (!isAddParticipants.value) { + contactsViewModel.createRoom( + CompanionClass.ROOM_TYPE_ONE_ONE, + contact.source!!, + contact.id!!, + null + ) + } else { + isSelected = !isSelected + if (isSelected) { + contactsViewModel.selectContact(contact) + contactsViewModel.updateAddButtonState() + } else { + contactsViewModel.deselectContact(contact) + contactsViewModel.updateAddButtonState() + } + } + } + ), + verticalAlignment = Alignment.CenterVertically + ) { + val imageUri = contact.id?.let { contactsViewModel.getImageUri(it, true) } + val errorPlaceholderImage: Int = R.drawable.account_circle_96dp + val loadedImage = loadImage(imageUri, context, errorPlaceholderImage) + AsyncImage( + model = loadedImage, + contentDescription = stringResource(R.string.user_avatar), + modifier = Modifier.size(width = 45.dp, height = 45.dp) + ) + Text(modifier = Modifier.padding(16.dp), text = contact.label!!) + if (isAddParticipants.value) { + if (isSelected) { + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_check_circle), + contentDescription = "Selected", + tint = Color.Blue, + modifier = Modifier.padding(end = 8.dp) + ) + } + } + } + when (roomUiState) { + is RoomUiState.Success -> { + val conversation = (roomUiState as RoomUiState.Success).conversation + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation?.token) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + context.startActivity(chatIntent) + } + is RoomUiState.Error -> { + val errorMessage = (roomUiState as RoomUiState.Error).message + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Error: $errorMessage", color = Color.Red) + } + } + is RoomUiState.None -> {} + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsAppBar.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsAppBar.kt new file mode 100644 index 0000000..8b916e2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsAppBar.kt @@ -0,0 +1,77 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.app.Activity +import android.content.Intent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.R +import com.nextcloud.talk.components.VerticallyCenteredRow +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ContactsAppBar(isAddParticipants: Boolean, autocompleteUsers: List, onStartSearch: () -> Unit) { + val context = LocalContext.current + TopAppBar( + modifier = Modifier + .height(60.dp), + title = { + VerticallyCenteredRow { + Text( + text = if (isAddParticipants) { + stringResource(R.string.nc_participants_add) + } else { + stringResource(R.string.nc_new_conversation) + } + ) + } + }, + navigationIcon = { + VerticallyCenteredRow { + IconButton(onClick = { (context as? Activity)?.finish() }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_button)) + } + } + }, + actions = { + VerticallyCenteredRow { + IconButton(onClick = onStartSearch) { + Icon(Icons.Filled.Search, contentDescription = stringResource(R.string.search_icon)) + } + if (isAddParticipants) { + Text( + text = stringResource(id = R.string.nc_contacts_done), + modifier = Modifier.clickable { + val resultIntent = Intent().apply { + putParcelableArrayListExtra("selectedParticipants", ArrayList(autocompleteUsers)) + } + (context as? Activity)?.setResult(Activity.RESULT_OK, resultIntent) + (context as? Activity)?.finish() + } + ) + } + } + } + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsItem.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsItem.kt new file mode 100644 index 0000000..1fae754 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsItem.kt @@ -0,0 +1,74 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.content.Context +import android.util.Log +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.contacts.CompanionClass +import com.nextcloud.talk.contacts.ContactsViewModel +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ContactsItem(contacts: List, contactsViewModel: ContactsViewModel, context: Context) { + val groupedContacts: Map> = contacts.groupBy { contact -> + ( + if (contact.source == "users") { + contact.label?.first()?.uppercase() + } else { + contact.source?.replaceFirstChar { actorType -> + actorType.uppercase() + } + } + ).toString() + } + LazyColumn( + modifier = Modifier + .fillMaxWidth(), + contentPadding = PaddingValues( + top = 10.dp, + bottom = 40.dp, + start = 10.dp, + end = 10.dp + ), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + groupedContacts.forEach { (initial, contactsForInitial) -> + stickyHeader { + Column { + Surface(Modifier.fillParentMaxWidth()) { + Header(initial) + } + HorizontalDivider(thickness = 0.1.dp, color = Color.Black) + } + } + items(contactsForInitial) { contact -> + ContactItemRow( + contact = contact, + contactsViewModel = contactsViewModel, + context = context + ) + Log.d(CompanionClass.TAG, "Contacts:$contact") + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsList.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsList.kt new file mode 100644 index 0000000..560a794 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsList.kt @@ -0,0 +1,59 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.util.Log +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import com.nextcloud.talk.contacts.CompanionClass +import com.nextcloud.talk.contacts.ContactsUiState +import com.nextcloud.talk.contacts.ContactsViewModel + +@Composable +fun ContactsList(contactsUiState: ContactsUiState, contactsViewModel: ContactsViewModel) { + val context = LocalContext.current + when (contactsUiState) { + is ContactsUiState.None -> { + } + + is ContactsUiState.Loading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is ContactsUiState.Success -> { + val contacts = contactsUiState.contacts + Log.d(CompanionClass.TAG, "Contacts:$contacts") + if (contacts != null) { + ContactsItem(contacts, contactsViewModel, context) + } + } + + is ContactsUiState.Error -> { + val errorMessage = contactsUiState.message + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Error: $errorMessage", color = Color.Red) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsSearchAppBar.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsSearchAppBar.kt new file mode 100644 index 0000000..b5da4b0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ContactsSearchAppBar.kt @@ -0,0 +1,117 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.R +import com.nextcloud.talk.components.VerticallyCenteredRow + +@Suppress("LongParameterList") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ContactsSearchAppBar( + searchQuery: String, + onTextChange: (String) -> Unit, + onCloseSearch: () -> Unit, + enableAddButton: Boolean, + isAddParticipants: Boolean, + clickAddButton: (Boolean) -> Unit +) { + val keyboardController = LocalSoftwareKeyboardController.current + + Surface( + modifier = Modifier.height(60.dp) + ) { + VerticallyCenteredRow { + IconButton( + modifier = Modifier.padding(start = 4.dp), + onClick = onCloseSearch + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back_button) + ) + } + + TextField( + value = searchQuery, + onValueChange = onTextChange, + placeholder = { Text(text = stringResource(R.string.nc_search)) }, + singleLine = true, + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = searchKeyboardActions(searchQuery, keyboardController), + colors = searchTextFieldColors(), + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { onTextChange("") }) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.nc_search_clear) + ) + } + } + } + ) + + if (isAddParticipants) { + TextButton( + onClick = { + onCloseSearch() + clickAddButton(true) + }, + enabled = enableAddButton + ) { + Text(text = stringResource(R.string.add_participants)) + } + } + } + } +} + +@Composable +fun searchTextFieldColors() = + TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ) + +fun searchKeyboardActions(text: String, keyboardController: SoftwareKeyboardController?) = + KeyboardActions( + onSearch = { + if (text.trim().isNotEmpty()) { + keyboardController?.hide() + } + } + ) diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt new file mode 100644 index 0000000..4f060d0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/ConversationCreationOptions.kt @@ -0,0 +1,92 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import android.content.Intent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.nextcloud.talk.R +import com.nextcloud.talk.conversationcreation.ConversationCreationActivity +import com.nextcloud.talk.openconversations.ListOpenConversationsActivity + +@Composable +fun ConversationCreationOptions() { + val context = LocalContext.current + Column { + Row( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp) + .clickable { + val intent = Intent(context, ConversationCreationActivity::class.java) + context.startActivity(intent) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.baseline_chat_bubble_outline_24), + modifier = Modifier + .width(40.dp) + .height(40.dp) + .padding(8.dp), + contentDescription = null + ) + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + text = stringResource(R.string.nc_create_new_conversation), + maxLines = 1, + fontSize = 16.sp + ) + } + Row( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp) + .clickable { + val intent = Intent(context, ListOpenConversationsActivity::class.java) + context.startActivity(intent) + }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.AutoMirrored.Filled.List, + modifier = Modifier + .width(40.dp) + .height(40.dp) + .padding(8.dp), + contentDescription = null + ) + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + text = stringResource(R.string.nc_join_open_conversations), + fontSize = 16.sp + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/contacts/components/Header.kt b/app/src/main/java/com/nextcloud/talk/contacts/components/Header.kt new file mode 100644 index 0000000..a142814 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/contacts/components/Header.kt @@ -0,0 +1,34 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.R + +@Composable +fun Header(header: String) { + Text( + text = header, + modifier = Modifier + .fillMaxSize() + .background(colorResource(id = R.color.bg_default)) + .padding(start = 60.dp), + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt new file mode 100644 index 0000000..c09c7c8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversation/RenameConversationDialogFragment.kt @@ -0,0 +1,227 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversation + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.res.ColorStateList +import android.os.Bundle +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel +import com.nextcloud.talk.conversationlist.ConversationsListActivity +import com.nextcloud.talk.databinding.DialogRenameConversationBinding +import com.nextcloud.talk.events.ConversationsListFetchDataEvent +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.vanniktech.emoji.EmojiPopup +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class RenameConversationDialogFragment : DialogFragment() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var eventBus: EventBus + + private lateinit var binding: DialogRenameConversationBinding + private lateinit var viewModel: ConversationInfoEditViewModel + + private var emojiPopup: EmojiPopup? = null + + private var roomToken = "" + private var initialName = "" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + viewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoEditViewModel::class.java] + roomToken = arguments?.getString(KEY_ROOM_TOKEN)!! + initialName = arguments?.getString(INITIAL_NAME)!! + } + + @SuppressLint("InflateParams") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogRenameConversationBinding.inflate(layoutInflater) + + val dialogBuilder = MaterialAlertDialogBuilder(binding.root.context) + .setTitle(resources.getString(R.string.nc_rename)) + // listener is null for now to avoid closing after button was clicked. + // listener is set later in onStart + .setPositiveButton(R.string.nc_rename_confirm, null) + .setNegativeButton(R.string.nc_common_dismiss, null) + .setView(binding.root) + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.root.context, dialogBuilder) + + return dialogBuilder.create() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + binding.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupListeners() + setupStateObserver() + + setupEmojiPopup() + } + + override fun onStart() { + super.onStart() + binding.textEdit.setText(initialName) + + val positiveButton = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) + positiveButton.isEnabled = false + positiveButton.setOnClickListener { + viewModel.renameRoom(roomToken, binding.textEdit.text.toString()) + } + + themeDialog() + } + + private fun themeDialog() { + viewThemeUtils.platform.themeDialog(binding.root) + viewThemeUtils.platform.colorTextButtons((dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)) + viewThemeUtils.platform.colorTextButtons((dialog as AlertDialog).getButton(AlertDialog.BUTTON_NEGATIVE)) + viewThemeUtils.material.colorTextInputLayout(binding.textInputLayout) + } + + private fun setupEmojiPopup() { + emojiPopup = binding.let { + EmojiPopup( + rootView = requireView(), + editText = it.textEdit, + onEmojiPopupShownListener = { + viewThemeUtils.platform.colorImageView(it.smileyButton, ColorRole.PRIMARY) + }, + onEmojiPopupDismissListener = { + it.smileyButton.imageTintList = ColorStateList.valueOf( + ResourcesCompat.getColor( + resources, + R.color.medium_emphasis_text, + context?.theme + ) + ) + }, + onEmojiClickListener = { + binding.textEdit.editableText?.append(" ") + } + ) + } + } + + private fun setupListeners() { + binding.smileyButton.setOnClickListener { emojiPopup?.toggle() } + binding.textEdit.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // unused atm + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + // unused atm + } + + override fun afterTextChanged(s: Editable) { + val positiveButton = (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) + + if (!TextUtils.isEmpty(s)) { + if (initialName == s.toString()) { + positiveButton.isEnabled = false + } else if (!positiveButton.isEnabled) { + positiveButton.isEnabled = true + } + } else { + if (positiveButton.isEnabled) { + positiveButton.isEnabled = false + } + } + } + }) + } + + private fun setupStateObserver() { + viewModel.renameRoomUiState.observe(viewLifecycleOwner) { state -> + when (state) { + is ConversationInfoEditViewModel.RenameRoomUiState.None -> { + } + is ConversationInfoEditViewModel.RenameRoomUiState.Success -> { + handleSuccess() + } + is ConversationInfoEditViewModel.RenameRoomUiState.Error -> { + showError() + } + } + } + } + + @SuppressLint("StringFormatInvalid") + private fun handleSuccess() { + eventBus.post(ConversationsListFetchDataEvent()) + + context?.resources?.let { + String.format( + it.getString(R.string.renamed_conversation), + initialName + ) + }?.let { + (activity as ConversationsListActivity?)?.showSnackbar( + it + ) + } + + dismiss() + } + + private fun showError() { + dismiss() + Log.e(TAG, "Failed to rename conversation") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + /** + * Fragment creator + */ + companion object { + private val TAG = RenameConversationDialogFragment::class.java.simpleName + private const val KEY_ROOM_TOKEN = "keyRoomToken" + private const val INITIAL_NAME = "initialName" + + @JvmStatic + fun newInstance(roomTokenParam: String, initialName: String): RenameConversationDialogFragment { + val args = Bundle() + args.putString(KEY_ROOM_TOKEN, roomTokenParam) + args.putString(INITIAL_NAME, initialName) + val fragment = RenameConversationDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt new file mode 100644 index 0000000..76feaa3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationActivity.kt @@ -0,0 +1,731 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +@file:Suppress("DEPRECATION") + +package com.nextcloud.talk.conversationcreation + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import coil.compose.AsyncImage +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.contacts.ContactsActivity +import com.nextcloud.talk.contacts.loadImage +import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.PickImage +import com.nextcloud.talk.utils.bundle.BundleKeys +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ConversationCreationActivity : BaseActivity() { + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + private lateinit var pickImage: PickImage + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + val conversationCreationViewModel = ViewModelProvider( + this, + viewModelFactory + )[ConversationCreationViewModel::class.java] + val conversationUser = conversationCreationViewModel.currentUser + pickImage = PickImage(this, conversationUser) + + setContent { + val colorScheme = viewThemeUtils.getColorScheme(this) + val context = LocalContext.current + MaterialTheme( + colorScheme = colorScheme + ) { + ConversationCreationScreen(conversationCreationViewModel, context, pickImage) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConversationCreationScreen( + conversationCreationViewModel: ConversationCreationViewModel, + context: Context, + pickImage: PickImage +) { + val selectedImageUri = conversationCreationViewModel.selectedImageUri.collectAsState().value + + val imagePickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + pickImage.onImagePickerResult(result.data) { uri -> + conversationCreationViewModel.updateSelectedImageUri(uri) + } + } + } + + val remoteFilePickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + pickImage.onSelectRemoteFilesResult(imagePickerLauncher, result.data) + } + } + + val cameraLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + pickImage.onTakePictureResult(imagePickerLauncher, result.data) + } + } + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data = result.data + val selectedParticipants = + data?.getParcelableArrayListExtraProvider("selectedParticipants") + ?: emptyList() + val participants = selectedParticipants.toMutableList() + conversationCreationViewModel.updateSelectedParticipants(participants) + } + } + ) + + ColoredStatusBar() + Scaffold( + modifier = Modifier + .statusBarsPadding() + .displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(text = stringResource(id = R.string.nc_new_conversation)) }, + navigationIcon = { + IconButton(onClick = { + (context as? Activity)?.finish() + }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.back_button) + ) + } + } + ) + }, + content = { paddingValues -> + Column( + modifier = Modifier + .background(colorResource(id = R.color.bg_default)) + .padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, paddingValues.calculateBottomPadding()) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + DefaultUserAvatar(selectedImageUri) + UploadAvatar( + pickImage = pickImage, + onImageSelected = { uri -> conversationCreationViewModel.updateSelectedImageUri(uri) }, + imagePickerLauncher = imagePickerLauncher, + remoteFilePickerLauncher = remoteFilePickerLauncher, + cameraLauncher = cameraLauncher, + onDeleteImage = { conversationCreationViewModel.updateSelectedImageUri(null) }, + selectedImageUri = selectedImageUri + ) + + ConversationNameAndDescription(conversationCreationViewModel) + AddParticipants(launcher, context, conversationCreationViewModel) + RoomCreationOptions(conversationCreationViewModel) + CreateConversation(conversationCreationViewModel, context) + } + } + ) +} + +@Composable +fun DefaultUserAvatar(selectedImageUri: Uri?) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + if (selectedImageUri != null) { + AsyncImage( + model = selectedImageUri, + contentDescription = stringResource(id = R.string.user_avatar), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(84.dp) + .padding(top = 8.dp) + .clip(CircleShape) + ) + } else { + AsyncImage( + model = R.drawable.ic_circular_group, + contentDescription = stringResource(id = R.string.user_avatar), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(84.dp) + .padding(top = 8.dp) + .clip(CircleShape) + ) + } + } +} + +@Composable +fun UploadAvatar( + pickImage: PickImage, + onImageSelected: (Uri) -> Unit, + imagePickerLauncher: ManagedActivityResultLauncher, + remoteFilePickerLauncher: ManagedActivityResultLauncher, + cameraLauncher: ManagedActivityResultLauncher, + onDeleteImage: () -> Unit, + selectedImageUri: Uri? +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.Center + ) { + IconButton( + onClick = { + pickImage.takePicture(cameraLauncher) + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_photo_camera_24), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + + IconButton(onClick = { + pickImage.selectLocal(imagePickerLauncher) + }) { + Icon( + painter = painterResource(id = R.drawable.upload), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + IconButton( + onClick = { + pickImage.selectRemote(remoteFilePickerLauncher) + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_folder), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + + if (selectedImageUri != null) { + IconButton(onClick = { + onDeleteImage() + }) { + Icon( + painter = painterResource(id = R.drawable.ic_delete_grey600_24dp), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } + } +} + +@Composable +fun ConversationNameAndDescription(conversationCreationViewModel: ConversationCreationViewModel) { + val conversationRoomName = conversationCreationViewModel.roomName.collectAsState() + val conversationDescription = conversationCreationViewModel.conversationDescription.collectAsState() + OutlinedTextField( + value = conversationRoomName.value, + onValueChange = { + conversationCreationViewModel.updateRoomName(it) + }, + label = { Text(text = stringResource(id = R.string.nc_call_name)) }, + modifier = Modifier + .padding(start = 16.dp, end = 16.dp) + .fillMaxWidth(), + singleLine = true + ) + OutlinedTextField( + value = conversationDescription.value, + onValueChange = { + if (it.length > CapabilitiesUtil.conversationDescriptionLength( + conversationCreationViewModel.currentUser + .capabilities?.spreedCapability!! + ) + ) { + conversationCreationViewModel.updateConversationDescription( + it.take( + CapabilitiesUtil.conversationDescriptionLength( + conversationCreationViewModel.currentUser + .capabilities?.spreedCapability!! + ) + ) + ) + } else { + conversationCreationViewModel.updateConversationDescription(it) + } + }, + label = { Text(text = stringResource(id = R.string.nc_conversation_description)) }, + modifier = Modifier + .padding(top = 8.dp, start = 16.dp, end = 16.dp) + .fillMaxWidth() + ) +} + +@SuppressLint("SuspiciousIndentation") +@Composable +fun AddParticipants( + launcher: ManagedActivityResultLauncher, + context: Context, + conversationCreationViewModel: ConversationCreationViewModel +) { + val participants = conversationCreationViewModel.selectedParticipants.collectAsState().value + + Column( + modifier = Modifier + .fillMaxHeight() + .padding(start = 16.dp, end = 16.dp, top = 16.dp) + ) { + Row { + Text( + text = stringResource(id = R.string.nc_participants).uppercase(), + fontSize = 14.sp, + modifier = Modifier.padding(start = 0.dp, bottom = 16.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + if (participants.isNotEmpty()) { + Text( + text = stringResource(id = R.string.nc_edit), + fontSize = 12.sp, + modifier = Modifier + .padding(start = 16.dp, bottom = 16.dp) + .clickable { + val intent = Intent(context, ContactsActivity::class.java) + intent.putParcelableArrayListExtra( + "selectedParticipants", + participants as ArrayList + ) + intent.putExtra(BundleKeys.KEY_ADD_PARTICIPANTS, true) + intent.putExtra("isAddParticipantsEdit", true) + launcher.launch(intent) + }, + textAlign = TextAlign.Right + ) + } + } + participants.toSet().forEach { participant -> + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + val imageUri = participant.id?.let { conversationCreationViewModel.getImageUri(it, true) } + val errorPlaceholderImage: Int = R.drawable.account_circle_96dp + val loadedImage = loadImage(imageUri, context, errorPlaceholderImage) + AsyncImage( + model = loadedImage, + contentDescription = stringResource(id = R.string.user_avatar), + modifier = Modifier.size(width = 32.dp, height = 32.dp) + ) + participant.label?.let { + Text( + text = it, + modifier = Modifier.padding(all = 16.dp), + fontSize = 15.sp + ) + } + } + HorizontalDivider(thickness = 0.1.dp, color = Color.Black) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + val intent = Intent(context, ContactsActivity::class.java) + intent.putExtra(BundleKeys.KEY_ADD_PARTICIPANTS, true) + launcher.launch(intent) + }, + verticalAlignment = Alignment.CenterVertically + ) { + if (participants.isEmpty()) { + Icon( + painter = painterResource(id = R.drawable.ic_account_plus), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Text( + text = stringResource(id = R.string.nc_add_participants), + modifier = Modifier.padding(start = 16.dp) + ) + } + } + } +} + +@Composable +fun RoomCreationOptions(conversationCreationViewModel: ConversationCreationViewModel) { + val isGuestsAllowed = conversationCreationViewModel.isGuestsAllowed.value + val isConversationAvailableForRegisteredUsers = conversationCreationViewModel + .isConversationAvailableForRegisteredUsers.value + val isOpenForGuestAppUsers = conversationCreationViewModel.openForGuestAppUsers.value + + val isPasswordSet = conversationCreationViewModel.isPasswordEnabled.value + + Text( + text = stringResource(id = R.string.nc_new_conversation_visibility).uppercase(), + fontSize = 14.sp, + modifier = Modifier.padding(top = 24.dp, start = 16.dp, end = 16.dp) + ) + ConversationOptions( + icon = R.drawable.ic_avatar_link, + text = R.string.nc_guest_access_allow_title, + switch = { + Switch( + checked = isGuestsAllowed, + onCheckedChange = { + conversationCreationViewModel.isGuestsAllowed.value = it + } + ) + }, + conversationCreationViewModel = conversationCreationViewModel + ) + + if (isGuestsAllowed && !isPasswordSet) { + ConversationOptions( + icon = R.drawable.baseline_lock_open_24, + text = R.string.nc_set_password, + conversationCreationViewModel = conversationCreationViewModel + ) + } + + if (isGuestsAllowed && isPasswordSet) { + ConversationOptions( + icon = R.drawable.ic_lock_grey600_24px, + text = R.string.nc_change_password, + conversationCreationViewModel = conversationCreationViewModel + ) + } + + ConversationOptions( + icon = R.drawable.baseline_format_list_bulleted_24, + text = R.string.nc_open_conversation_to_registered_users, + switch = { + Switch( + checked = isConversationAvailableForRegisteredUsers, + onCheckedChange = { + conversationCreationViewModel.isConversationAvailableForRegisteredUsers.value = it + } + ) + }, + conversationCreationViewModel = conversationCreationViewModel + ) + + if (isConversationAvailableForRegisteredUsers) { + ConversationOptions( + text = R.string.nc_open_to_guest_app_users, + switch = { + Switch( + checked = isOpenForGuestAppUsers, + onCheckedChange = { + conversationCreationViewModel.openForGuestAppUsers.value = it + } + ) + }, + conversationCreationViewModel = conversationCreationViewModel + ) + } +} + +@Composable +fun ConversationOptions( + icon: Int? = null, + text: Int, + switch: @Composable (() -> Unit)? = null, + conversationCreationViewModel: ConversationCreationViewModel +) { + var showPasswordDialog by rememberSaveable { mutableStateOf(false) } + var showPasswordChangeDialog by rememberSaveable { mutableStateOf(false) } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + .then( + if (!conversationCreationViewModel.isPasswordEnabled.value) { + Modifier.clickable { + showPasswordDialog = true + } + } else if (conversationCreationViewModel.isPasswordEnabled.value) { + Modifier.clickable { + showPasswordChangeDialog = true + } + } else { + Modifier + } + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + } else { + Spacer(modifier = Modifier.width(40.dp)) + } + Text( + text = stringResource(id = text), + modifier = Modifier.weight(1f) + ) + if (switch != null) { + switch() + } + if (showPasswordDialog) { + ShowPasswordDialog( + onDismiss = { showPasswordDialog = false }, + conversationCreationViewModel = conversationCreationViewModel + ) + } + if (showPasswordChangeDialog) { + ShowChangePassword( + onDismiss = { + showPasswordChangeDialog = false + }, + conversationCreationViewModel = conversationCreationViewModel + ) + } + } +} + +@Composable +fun ShowChangePassword(onDismiss: () -> Unit, conversationCreationViewModel: ConversationCreationViewModel) { + var changedPassword by rememberSaveable { mutableStateOf("") } + Dialog(onDismissRequest = { + onDismiss() + }) { + Card( + modifier = Modifier + .fillMaxWidth() + .height(375.dp) + .padding(32.dp) + .clip(RoundedCornerShape(16.dp)) + .background(color = colorResource(id = R.color.appbar)) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 16.dp, horizontal = 16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = stringResource(id = R.string.nc_set_new_password), fontWeight = FontWeight.SemiBold) + Spacer(modifier = Modifier.height(16.dp)) + OutlinedTextField( + value = changedPassword, + onValueChange = { + changedPassword = it + }, + label = { Text(text = stringResource(id = R.string.nc_password)) }, + singleLine = true + ) + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextButton( + onClick = { + conversationCreationViewModel.updatePassword(changedPassword) + conversationCreationViewModel.isPasswordEnabled.value = true + onDismiss() + }, + enabled = changedPassword.isNotEmpty() && changedPassword.isNotBlank(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + ) { + Text(text = stringResource(id = R.string.nc_change_password)) + } + Spacer(modifier = Modifier.height(4.dp)) + TextButton( + onClick = { + conversationCreationViewModel.isPasswordEnabled.value = false + onDismiss() + }, + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + ) { + Text( + text = stringResource(id = R.string.nc_remove_password), + color = colorResource(id = R.color.nc_darkRed) + ) + } + Spacer(modifier = Modifier.height(4.dp)) + TextButton( + onClick = { onDismiss() }, + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + ) { + Text(text = stringResource(id = R.string.nc_cancel)) + } + } + } + } + } +} + +@Composable +fun ShowPasswordDialog(onDismiss: () -> Unit, conversationCreationViewModel: ConversationCreationViewModel) { + var password by rememberSaveable { mutableStateOf("") } + AlertDialog( + containerColor = colorResource(id = R.color.dialog_background), + onDismissRequest = onDismiss, + title = { Text(text = stringResource(id = R.string.nc_set_password)) }, + text = { + TextField( + value = password, + onValueChange = { + password = it + }, + label = { Text(text = stringResource(id = R.string.nc_guest_access_password_dialog_hint)) } + ) + }, + confirmButton = { + TextButton( + onClick = { + if (password.isNotEmpty() && password.isNotBlank()) { + conversationCreationViewModel.updatePassword(password) + conversationCreationViewModel.isPasswordEnabled(true) + } + } + ) { + Text(text = stringResource(id = R.string.save)) + } + }, + dismissButton = { + TextButton(onClick = { onDismiss() }) { + Text(text = stringResource(id = R.string.nc_cancel)) + } + } + ) +} + +@Composable +fun CreateConversation(conversationCreationViewModel: ConversationCreationViewModel, context: Context) { + val selectedParticipants by conversationCreationViewModel.selectedParticipants.collectAsState() + Box( + modifier = Modifier + .fillMaxWidth() + .padding(all = 16.dp), + contentAlignment = Alignment.Center + ) { + Button( + onClick = { + conversationCreationViewModel.createRoomAndAddParticipants( + roomType = CompanionClass.ROOM_TYPE_GROUP, + conversationName = conversationCreationViewModel.roomName.value, + participants = selectedParticipants.toSet() + ) { roomToken -> + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + context.startActivity(chatIntent) + } + } + ) { + Text(text = stringResource(id = R.string.create_conversation)) + } + } +} + +class CompanionClass { + companion object { + internal val TAG = ConversationCreationActivity::class.simpleName + internal const val ROOM_TYPE_GROUP = "2" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt new file mode 100644 index 0000000..12c9dba --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepository.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationcreation + +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.participants.AddParticipantOverall +import java.io.File + +interface ConversationCreationRepository { + + suspend fun allowGuests(token: String, allow: Boolean): GenericOverall + suspend fun renameConversation(roomToken: String, roomNameNew: String?): GenericOverall + suspend fun setConversationDescription(roomToken: String, description: String?): GenericOverall + suspend fun openConversation(roomToken: String, scope: Int): GenericOverall + suspend fun addParticipants(conversationToken: String?, userId: String, sourceType: String): AddParticipantOverall + suspend fun createRoom(roomType: String, conversationName: String?): RoomOverall + fun getImageUri(avatarId: String, requestBigSize: Boolean): String + suspend fun setPassword(roomToken: String, password: String): GenericOverall + suspend fun uploadConversationAvatar(file: File, roomToken: String): ConversationModel + suspend fun deleteConversationAvatar(roomToken: String): ConversationModel +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt new file mode 100644 index 0000000..668743a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationRepositoryImpl.kt @@ -0,0 +1,177 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationcreation + +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.RetrofitBucket +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.participants.AddParticipantOverall +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ApiUtils.getRetrofitBucketForAddParticipant +import com.nextcloud.talk.utils.ApiUtils.getRetrofitBucketForAddParticipantWithSource +import com.nextcloud.talk.utils.Mimetype +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import javax.inject.Inject + +class ConversationCreationRepositoryImpl @Inject constructor( + private val ncApiCoroutines: NcApiCoroutines, + currentUserProvider: CurrentUserProviderNew +) : ConversationCreationRepository { + private val _currentUser = currentUserProvider.currentUser.blockingGet() + val currentUser: User = _currentUser + val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) + val apiVersion = ApiUtils.getConversationApiVersion(_currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) + + override suspend fun renameConversation(roomToken: String, roomNameNew: String?): GenericOverall = + ncApiCoroutines.renameRoom( + credentials, + ApiUtils.getUrlForRoom( + apiVersion, + _currentUser.baseUrl, + roomToken + ), + roomNameNew + ) + + override suspend fun setConversationDescription(roomToken: String, description: String?): GenericOverall = + ncApiCoroutines.setConversationDescription( + credentials, + ApiUtils.getUrlForConversationDescription( + apiVersion, + _currentUser.baseUrl, + roomToken + ), + description + ) + + override suspend fun openConversation(roomToken: String, scope: Int): GenericOverall = + ncApiCoroutines.openConversation( + credentials, + ApiUtils.getUrlForOpeningConversations( + apiVersion, + _currentUser.baseUrl, + roomToken + ), + scope + ) + + override suspend fun addParticipants( + conversationToken: String?, + userId: String, + sourceType: String + ): AddParticipantOverall { + val retrofitBucket: RetrofitBucket = if (sourceType == "users") { + getRetrofitBucketForAddParticipant( + apiVersion, + _currentUser.baseUrl, + conversationToken, + userId + ) + } else { + getRetrofitBucketForAddParticipantWithSource( + apiVersion, + _currentUser.baseUrl, + conversationToken, + sourceType, + userId + ) + } + val participants = ncApiCoroutines.addParticipant(credentials, retrofitBucket.url, retrofitBucket.queryMap) + return participants + } + + override suspend fun createRoom(roomType: String, conversationName: String?): RoomOverall { + val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + version = apiVersion, + baseUrl = _currentUser.baseUrl, + roomType = roomType, + conversationName = conversationName + ) + val response = ncApiCoroutines.createRoom( + credentials, + retrofitBucket.url, + retrofitBucket.queryMap + ) + return response + } + + override fun getImageUri(avatarId: String, requestBigSize: Boolean): String = + ApiUtils.getUrlForAvatar( + _currentUser.baseUrl, + avatarId, + requestBigSize + ) + + override suspend fun setPassword(roomToken: String, password: String): GenericOverall { + val result = ncApiCoroutines.setPassword( + credentials, + ApiUtils.getUrlForRoomPassword( + apiVersion, + _currentUser.baseUrl!!, + roomToken + ), + password + ) + return result + } + + override suspend fun uploadConversationAvatar(file: File, roomToken: String): ConversationModel { + val builder = MultipartBody.Builder() + builder.setType(MultipartBody.FORM) + builder.addFormDataPart( + "file", + file.name, + file.asRequestBody(Mimetype.IMAGE_PREFIX_GENERIC.toMediaTypeOrNull()) + ) + val filePart: MultipartBody.Part = MultipartBody.Part.createFormData( + "file", + file.name, + file.asRequestBody(Mimetype.IMAGE_JPG.toMediaTypeOrNull()) + ) + val response = ncApiCoroutines.uploadConversationAvatar( + credentials!!, + ApiUtils.getUrlForConversationAvatar(1, _currentUser.baseUrl!!, roomToken), + filePart + ) + return ConversationModel.mapToConversationModel(response.ocs?.data!!, _currentUser) + } + + override suspend fun deleteConversationAvatar(roomToken: String): ConversationModel { + val url = ApiUtils.getUrlForConversationAvatar(1, _currentUser.baseUrl!!, roomToken) + val response = ncApiCoroutines.deleteConversationAvatar(credentials!!, url) + return ConversationModel.mapToConversationModel(response.ocs?.data!!, _currentUser) + } + + override suspend fun allowGuests(token: String, allow: Boolean): GenericOverall { + val url = ApiUtils.getUrlForRoomPublic( + apiVersion, + _currentUser.baseUrl!!, + token + ) + + val result: GenericOverall = if (allow) { + ncApiCoroutines.makeRoomPublic( + credentials!!, + url + ) + } else { + ncApiCoroutines.makeRoomPrivate( + credentials!!, + url + ) + } + return result + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt new file mode 100644 index 0000000..9c6e2bf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationcreation/ConversationCreationViewModel.kt @@ -0,0 +1,162 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationcreation + +import android.net.Uri +import android.util.Log +import androidx.compose.runtime.mutableStateOf +import androidx.core.net.toFile +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.generic.GenericMeta +import com.nextcloud.talk.repositories.conversations.ConversationsRepositoryImpl.Companion.STATUS_CODE_OK +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ConversationCreationViewModel @Inject constructor( + private val repository: ConversationCreationRepository, + private val currentUserProvider: CurrentUserProviderNew +) : ViewModel() { + private val _selectedParticipants = MutableStateFlow>(emptyList()) + val selectedParticipants: StateFlow> = _selectedParticipants + private val roomViewState = MutableStateFlow(RoomUIState.None) + + private val _selectedImageUri = MutableStateFlow(null) + val selectedImageUri: StateFlow = _selectedImageUri + + private val _currentUser = currentUserProvider.currentUser.blockingGet() + val currentUser: User = _currentUser + + private val _isPasswordEnabled = mutableStateOf(false) + val isPasswordEnabled = _isPasswordEnabled + + fun updateSelectedParticipants(participants: List) { + _selectedParticipants.value = participants + } + + fun isPasswordEnabled(value: Boolean) { + _isPasswordEnabled.value = value + } + + fun updateSelectedImageUri(uri: Uri?) { + _selectedImageUri.value = uri + } + private val _roomName = MutableStateFlow("") + val roomName: StateFlow = _roomName + private val _password = MutableStateFlow("") + val password: StateFlow = _password + private val _conversationDescription = MutableStateFlow("") + val conversationDescription: StateFlow = _conversationDescription + var isGuestsAllowed = mutableStateOf(false) + var isConversationAvailableForRegisteredUsers = mutableStateOf(false) + var openForGuestAppUsers = mutableStateOf(false) + private val addParticipantsViewState = MutableStateFlow(AddParticipantsUiState.None) + private val allowGuestsResult = MutableStateFlow(AllowGuestsUiState.None) + fun updateRoomName(roomName: String) { + _roomName.value = roomName + } + + fun updatePassword(password: String) { + _password.value = password + } + + fun updateConversationDescription(conversationDescription: String) { + _conversationDescription.value = conversationDescription + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun createRoomAndAddParticipants( + roomType: String, + conversationName: String, + participants: Set, + onRoomCreated: (String) -> Unit + ) { + val scope = when { + isConversationAvailableForRegisteredUsers.value && !openForGuestAppUsers.value -> 1 + isConversationAvailableForRegisteredUsers.value && openForGuestAppUsers.value -> 2 + else -> 0 + } + viewModelScope.launch { + roomViewState.value = RoomUIState.None + try { + val roomResult = repository.createRoom(roomType, conversationName) + val conversation = roomResult.ocs?.data + + if (conversation != null) { + val token = conversation.token + if (token != null) { + try { + repository.setConversationDescription( + token, + _conversationDescription.value + ) + val allowGuestResultOverall = repository.allowGuests(token, isGuestsAllowed.value) + val statusCode: GenericMeta? = allowGuestResultOverall.ocs?.meta + val result = (statusCode?.statusCode == STATUS_CODE_OK) + if (result) { + allowGuestsResult.value = AllowGuestsUiState.Success(result) + for (participant in participants) { + if (participant.id != null) { + val participantOverall = repository.addParticipants( + token, + participant.id!!, + participant.source!! + ).ocs?.data + addParticipantsViewState.value = + AddParticipantsUiState.Success(participantOverall) + } + } + } + if (_password.value.isNotEmpty()) { + repository.setPassword(token, _password.value) + } + repository.openConversation(token, scope) + selectedImageUri.value?.let { repository.uploadConversationAvatar(it.toFile(), token) } + onRoomCreated(token) + } catch (exception: Exception) { + allowGuestsResult.value = AllowGuestsUiState.Error(exception.message ?: "") + } + } + roomViewState.value = RoomUIState.Success(conversation) + } else { + roomViewState.value = RoomUIState.Error("Conversation is null") + } + } catch (e: Exception) { + roomViewState.value = RoomUIState.Error(e.message ?: "Unknown error") + Log.e("ConversationCreationViewModel", "Error - ${e.message}") + } + } + } + + fun getImageUri(avatarId: String, requestBigSize: Boolean): String = + repository.getImageUri(avatarId, requestBigSize) +} + +sealed class AllowGuestsUiState { + data object None : AllowGuestsUiState() + data class Success(val result: Boolean) : AllowGuestsUiState() + data class Error(val message: String) : AllowGuestsUiState() +} + +sealed class RoomUIState { + data object None : RoomUIState() + data class Success(val conversation: Conversation?) : RoomUIState() + data class Error(val message: String) : RoomUIState() +} + +sealed class AddParticipantsUiState { + data object None : AddParticipantsUiState() + data class Success(val participants: List?) : AddParticipantsUiState() + data class Error(val message: String) : AddParticipantsUiState() +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt new file mode 100644 index 0000000..347ae60 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -0,0 +1,1953 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2021-2023 Andy Scherzinger + * SPDX-FileCopyrightText: 2021-2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2021-2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationinfo + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.DrawableRes +import androidx.appcompat.app.AlertDialog +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.toDrawable +import androidx.fragment.app.FragmentTransaction +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.afollestad.materialdialogs.LayoutMode.WRAP_CONTENT +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.bottomsheets.BottomSheet +import com.afollestad.materialdialogs.datetime.datePicker +import com.afollestad.materialdialogs.datetime.timePicker +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.activities.MainActivity +import com.nextcloud.talk.adapters.items.ParticipantItem +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.bottomsheet.items.BasicListItemWithImage +import com.nextcloud.talk.bottomsheet.items.listItemsWithImage +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS +import com.nextcloud.talk.contacts.ContactsActivity +import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel +import com.nextcloud.talk.conversationinfoedit.ConversationInfoEditActivity +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityConversationInfoBinding +import com.nextcloud.talk.databinding.DialogBanParticipantBinding +import com.nextcloud.talk.events.EventStatus +import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider +import com.nextcloud.talk.extensions.loadConversationAvatar +import com.nextcloud.talk.extensions.loadNoteToSelfAvatar +import com.nextcloud.talk.extensions.loadSystemAvatar +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.jobs.AddParticipantsToConversationWorker +import com.nextcloud.talk.jobs.DeleteConversationWorker +import com.nextcloud.talk.jobs.LeaveConversationWorker +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.domain.converters.DomainEnumNotificationLevelConverter +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.models.json.participants.Participant.ActorType.CIRCLES +import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS +import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS +import com.nextcloud.talk.models.json.participants.ParticipantsOverall +import com.nextcloud.talk.repositories.conversations.ConversationsRepository +import com.nextcloud.talk.shareditems.activities.SharedItemsActivity +import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity +import com.nextcloud.talk.ui.dialog.DialogBanListFragment +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.DateConstants +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.ShareUtils +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.time.Instant +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Calendar +import java.util.Collections +import java.util.Locale +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ConversationInfoActivity : + BaseActivity(), + FlexibleAdapter.OnItemClickListener { + private lateinit var binding: ActivityConversationInfoBinding + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var conversationsRepository: ConversationsRepository + + @Inject + lateinit var dateUtils: DateUtils + + lateinit var viewModel: ConversationInfoViewModel + + private lateinit var spreedCapabilities: SpreedCapability + + private lateinit var conversationToken: String + private lateinit var conversationUser: User + private var hasAvatarSpacing: Boolean = false + private lateinit var credentials: String + private var participantsDisposable: Disposable? = null + + private var databaseStorageModule: DatabaseStorageModule? = null + private var conversation: ConversationModel? = null + + private var adapter: FlexibleAdapter? = null + private var userItems: MutableList = ArrayList() + + private var startGroupChat: Boolean = false + + private lateinit var optionsMenu: Menu + + private val workerData: Data? + get() { + if (!TextUtils.isEmpty(conversationToken)) { + val data = Data.Builder() + data.putString(KEY_ROOM_TOKEN, conversationToken) + data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!) + return data.build() + } + return null + } + + private val addParticipantsResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { it -> + executeIfResultOk(it) { intent -> + val selectedAutocompleteUsers = + intent?.getParcelableArrayListExtraProvider("selectedParticipants") + ?: emptyList() + + if (startGroupChat) { + viewModel.createRoomFromOneToOne( + conversationUser, + userItems.map { it.model }, + selectedAutocompleteUsers, + conversationToken + ) + } else { + addParticipantsToConversation(selectedAutocompleteUsers) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityConversationInfoBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + initSystemBars() + + viewModel = + ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java] + + conversationUser = currentUserProvider.currentUser.blockingGet() + + conversationToken = intent.getStringExtra(KEY_ROOM_TOKEN)!! + hasAvatarSpacing = intent.getBooleanExtra(BundleKeys.KEY_ROOM_ONE_TO_ONE, false) + credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)!! + } + + override fun onStart() { + super.onStart() + this.lifecycle.addObserver(ConversationInfoViewModel.LifeCycleObserver) + } + + override fun onStop() { + super.onStop() + this.lifecycle.removeObserver(ConversationInfoViewModel.LifeCycleObserver) + } + + override fun onResume() { + super.onResume() + + if (databaseStorageModule == null) { + databaseStorageModule = DatabaseStorageModule(conversationUser, conversationToken) + } + + binding.deleteConversationAction.setOnClickListener { showDeleteConversationDialog() } + binding.leaveConversationAction.setOnClickListener { leaveConversation() } + binding.clearConversationHistory.setOnClickListener { showClearHistoryDialog() } + binding.addParticipantsAction.setOnClickListener { + startGroupChat = false + selectParticipantsToAdd() + } + binding.startGroupChat.setOnClickListener { + startGroupChat = true + selectParticipantsToAdd() + } + binding.listBansButton.setOnClickListener { listBans() } + + viewModel.getRoom(conversationUser, conversationToken) + + themeTextViews() + themeSwitchPreferences() + + binding.addParticipantsAction.visibility = GONE + + binding.progressBar.let { viewThemeUtils.platform.colorCircularProgressBar(it, ColorRole.PRIMARY) } + initObservers() + } + + private fun initObservers() { + initViewStateObserver() + initCapabilitiesObersver() + initRoomOberserver() + initBanActorObserver() + initConversationReadOnlyObserver() + initClearChatHistoryObserver() + initMarkConversationAsSensitiveObserver() + initMarkConversationAsInsensitiveObserver() + initMarkConversationAsImportantObserver() + initMarkConversationAsUnimportantObserver() + } + + private fun initMarkConversationAsSensitiveObserver() { + viewModel.markAsSensitiveResult.observe(this) { uiState -> + when (uiState) { + is ConversationInfoViewModel.MarkConversationAsSensitiveViewState.Success -> { + Snackbar.make( + binding.root, + context.getString(R.string.nc_mark_conversation_as_sensitive), + Snackbar.LENGTH_LONG + ).show() + } + is ConversationInfoViewModel.MarkConversationAsSensitiveViewState.Error -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Log.e(TAG, "failed to mark conversation as insensitive", uiState.exception) + } + else -> { + } + } + } + } + + private fun initMarkConversationAsInsensitiveObserver() { + viewModel.markAsInsensitiveResult.observe(this) { uiState -> + when (uiState) { + is ConversationInfoViewModel.MarkConversationAsInsensitiveViewState.Success -> { + Snackbar.make( + binding.root, + context.getString(R.string.nc_mark_conversation_as_insensitive), + Snackbar.LENGTH_LONG + ).show() + } + is ConversationInfoViewModel.MarkConversationAsInsensitiveViewState.Error -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Log.e(TAG, "failed to mark conversation as sensitive", uiState.exception) + } + else -> { + } + } + } + } + private fun initClearChatHistoryObserver() { + viewModel.clearChatHistoryViewState.observe(this) { uiState -> + when (uiState) { + is ConversationInfoViewModel.ClearChatHistoryViewState.None -> { + } + + is ConversationInfoViewModel.ClearChatHistoryViewState.Success -> { + Snackbar.make( + binding.root, + context.getString(R.string.nc_clear_history_success), + Snackbar.LENGTH_LONG + ).show() + } + + is ConversationInfoViewModel.ClearChatHistoryViewState.Error -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Log.e(TAG, "failed to clear chat history", uiState.exception) + } + } + } + } + + private fun initConversationReadOnlyObserver() { + viewModel.getConversationReadOnlyState.observe(this) { state -> + when (state) { + is ConversationInfoViewModel.SetConversationReadOnlyViewState.Success -> { + } + + is ConversationInfoViewModel.SetConversationReadOnlyViewState.Error -> { + Snackbar.make(binding.root, R.string.conversation_read_only_failed, Snackbar.LENGTH_LONG).show() + } + + is ConversationInfoViewModel.SetConversationReadOnlyViewState.None -> { + } + } + } + } + + private fun initBanActorObserver() { + viewModel.getBanActorState.observe(this) { state -> + when (state) { + is ConversationInfoViewModel.BanActorSuccessState -> { + getListOfParticipants() // Refresh the list of participants + } + + ConversationInfoViewModel.BanActorErrorState -> { + Snackbar.make(binding.root, "Error banning actor", Snackbar.LENGTH_SHORT).show() + } + + else -> {} + } + } + } + + private fun initRoomOberserver() { + viewModel.createRoomViewState.observe(this) { state -> + when (state) { + is ConversationInfoViewModel.CreateRoomUIState.Success -> { + state.room.ocs?.data?.token?.let { token -> + val chatIntent = Intent(context, ChatActivity::class.java).apply { + putExtra(KEY_ROOM_TOKEN, token) + } + startActivity(chatIntent) + } + } + + is ConversationInfoViewModel.CreateRoomUIState.Error -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + } + + private fun initCapabilitiesObersver() { + viewModel.getCapabilitiesViewState.observe(this) { state -> + when (state) { + is ConversationInfoViewModel.GetCapabilitiesSuccessState -> { + spreedCapabilities = state.spreedCapabilities + handleConversation() + } + + else -> {} + } + } + } + + private fun initMarkConversationAsImportantObserver() { + viewModel.markAsImportantResult.observe(this) { uiState -> + when (uiState) { + is ConversationInfoViewModel.MarkConversationAsImportantViewState.Success -> { + Snackbar.make( + binding.root, + context.getString(R.string.nc_mark_conversation_as_important), + Snackbar.LENGTH_LONG + ).show() + } + is ConversationInfoViewModel.MarkConversationAsImportantViewState.Error -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Log.e(TAG, "failed to mark conversation as important", uiState.exception) + } + else -> { + } + } + } + } + + private fun initMarkConversationAsUnimportantObserver() { + viewModel.markAsUnimportantResult.observe(this) { uiState -> + when (uiState) { + is ConversationInfoViewModel.MarkConversationAsUnimportantViewState.Success -> { + Snackbar.make( + binding.root, + context.getString(R.string.nc_mark_conversation_as_unimportant), + Snackbar.LENGTH_LONG + ).show() + } + is ConversationInfoViewModel.MarkConversationAsUnimportantViewState.Error -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Log.e(TAG, "failed to mark conversation as unimportant", uiState.exception) + } + else -> { + } + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun initViewStateObserver() { + viewModel.viewState.observe(this) { state -> + when (state) { + is ConversationInfoViewModel.GetRoomSuccessState -> { + conversation = state.conversationModel + viewModel.getCapabilities(conversationUser, conversationToken, conversation!!) + if (ConversationUtils.isNoteToSelfConversation(conversation)) { + binding.shareConversationButton.visibility = GONE + } + val canGeneratePrettyURL = CapabilitiesUtil.canGeneratePrettyURL(conversationUser) + binding.shareConversationButton.setOnClickListener { + ShareUtils.shareConversationLink( + this, + conversationUser.baseUrl, + conversation?.token, + conversation?.name, + canGeneratePrettyURL + ) + } + + conversation?.let { + if (it.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + viewModel.getProfileData(conversationUser, it.name) + } + } + } + + is ConversationInfoViewModel.GetRoomErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + + viewModel.getProfileViewState.observe(this) { state -> + when (state) { + is ConversationInfoViewModel.GetProfileSuccessState -> { + try { + // Pronouns + val profile = state.profile + val pronouns = profile.pronouns ?: "" + binding.pronouns.text = pronouns + + // Role @ Organization + val concat1 = if (profile.role != null && profile.company != null) " @ " else "" + val role = profile.role ?: "" + val company = profile.company ?: "" + val professionCompanyText = "$role$concat1$company" + binding.professionCompany.text = professionCompanyText + + // Local Time: xX:xX · Address + val profileZoneOffset = ZoneOffset.ofTotalSeconds(0) + val secondsToAdd = profile.timezoneOffset?.toLong() ?: 0 + val localTime = ZonedDateTime.ofInstant( + Instant.now().plusSeconds(secondsToAdd), + profileZoneOffset + ) + val localTimeString = localTime.format( + DateTimeFormatter + .ofLocalizedTime(FormatStyle.SHORT) + .withLocale(Locale.getDefault()) + ) + val concat2 = if (profile.address != null) " · " else "" + val address = profile.address ?: "" + val localTimeLocation = "$localTimeString$concat2$address" + binding.locationTime.text = resources.getString(R.string.local_time, localTimeLocation) + + binding.pronouns.visibility = VISIBLE + binding.professionCompany.visibility = if (professionCompanyText.isNotEmpty()) VISIBLE else GONE + binding.locationTime.visibility = VISIBLE + } catch (e: Exception) { + Log.e(TAG, "Exception getting profile information", e) + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + } + + is ConversationInfoViewModel.GetProfileErrorState -> { + Log.e(TAG, "Network error occurred getting profile information") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + } + + private fun setupActionBar() { + setSupportActionBar(binding.conversationInfoToolbar) + binding.conversationInfoToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(android.R.color.transparent, null).toDrawable()) + supportActionBar?.title = if (hasAvatarSpacing) { + " " + resources!!.getString(R.string.nc_conversation_menu_conversation_info) + } else { + resources!!.getString(R.string.nc_conversation_menu_conversation_info) + } + viewThemeUtils.material.themeToolbar(binding.conversationInfoToolbar) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + optionsMenu = menu + return true + } + + fun showOptionsMenu() { + if (::optionsMenu.isInitialized) { + optionsMenu.clear() + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.AVATAR)) { + menuInflater.inflate(R.menu.menu_conversation_info, optionsMenu) + } + } + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + super.onPrepareOptionsMenu(menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.edit) { + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, conversationToken) + + val intent = Intent(this, ConversationInfoEditActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } + return true + } + + private fun themeSwitchPreferences() { + binding.run { + listOf( + binding.webinarInfoView.lobbySwitch, + binding.notificationSettingsView.callNotificationsSwitch, + binding.notificationSettingsView.importantConversationSwitch, + binding.guestAccessView.allowGuestsSwitch, + binding.guestAccessView.passwordProtectionSwitch, + binding.recordingConsentView.recordingConsentForConversationSwitch, + binding.lockConversationSwitch, + binding.notificationSettingsView.sensitiveConversationSwitch + ).forEach(viewThemeUtils.talk::colorSwitch) + } + } + + private fun themeTextViews() { + binding.run { + listOf( + binding.notificationSettingsView.notificationSettingsCategory, + binding.webinarInfoView.webinarSettingsCategory, + binding.guestAccessView.guestAccessSettingsCategory, + binding.sharedItemsTitle, + binding.recordingConsentView.recordingConsentSettingsCategory, + binding.conversationSettingsTitle, + binding.participantsListCategory + ) + }.forEach(viewThemeUtils.platform::colorTextView) + } + + private fun showSharedItems() { + val intent = Intent(this, SharedItemsActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.putExtra(BundleKeys.KEY_CONVERSATION_NAME, conversation?.displayName) + intent.putExtra(KEY_ROOM_TOKEN, conversationToken) + intent.putExtra( + SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR, + ConversationUtils.isParticipantOwnerOrModerator(conversation!!) + ) + startActivity(intent) + } + + fun openThreadsOverview() { + val threadsUrl = ApiUtils.getUrlForRecentThreads( + version = 1, + baseUrl = conversationUser.baseUrl, + token = conversationToken + ) + + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, conversationToken) + bundle.putString(ThreadsOverviewActivity.KEY_APPBAR_TITLE, getString(R.string.recent_threads)) + bundle.putString(ThreadsOverviewActivity.KEY_THREADS_SOURCE_URL, threadsUrl) + val threadsOverviewIntent = Intent(context, ThreadsOverviewActivity::class.java) + threadsOverviewIntent.putExtras(bundle) + startActivity(threadsOverviewIntent) + } + + private fun setupWebinaryView() { + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) && + webinaryRoomType(conversation!!) && + ConversationUtils.canModerate(conversation!!, spreedCapabilities) + ) { + binding.webinarInfoView.webinarSettings.visibility = VISIBLE + + val isLobbyOpenToModeratorsOnly = + conversation!!.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY + binding.webinarInfoView.lobbySwitch.isChecked = isLobbyOpenToModeratorsOnly + + reconfigureLobbyTimerView() + + binding.webinarInfoView.startTimeButton.setOnClickListener { + MaterialDialog(this, BottomSheet(WRAP_CONTENT)).show { + val currentTimeCalendar = Calendar.getInstance() + if (conversation!!.lobbyTimer != 0L) { + currentTimeCalendar.timeInMillis = conversation!!.lobbyTimer * DateConstants.SECOND_DIVIDER + } + + datePicker { _, date -> + showTimePicker(date) + } + } + } + + binding.webinarInfoView.webinarSettingsLobby.setOnClickListener { + binding.webinarInfoView.lobbySwitch.isChecked = !binding.webinarInfoView.lobbySwitch.isChecked + reconfigureLobbyTimerView() + submitLobbyChanges() + } + } else { + binding.webinarInfoView.webinarSettings.visibility = GONE + } + } + + private fun showTimePicker(selectedDate: Calendar) { + val currentTime = Calendar.getInstance() + MaterialDialog(this, BottomSheet(WRAP_CONTENT)).show { + cancelable(false) + timePicker( + currentTime = Calendar.getInstance(), + show24HoursView = true, + timeCallback = { _, time -> + selectedDate.set(Calendar.HOUR_OF_DAY, time.get(Calendar.HOUR_OF_DAY)) + selectedDate.set(Calendar.MINUTE, time.get(Calendar.MINUTE)) + if (selectedDate.timeInMillis < currentTime.timeInMillis) { + selectedDate.set(Calendar.HOUR_OF_DAY, currentTime.get(Calendar.HOUR_OF_DAY)) + selectedDate.set(Calendar.MINUTE, currentTime.get(Calendar.MINUTE)) + } + reconfigureLobbyTimerView(selectedDate) + submitLobbyChanges() + } + ) + } + } + + private fun webinaryRoomType(conversation: ConversationModel): Boolean = + conversation.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL || + conversation.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL + + private fun reconfigureLobbyTimerView(dateTime: Calendar? = null) { + val isChecked = binding.webinarInfoView.lobbySwitch.isChecked + + if (dateTime != null && isChecked) { + conversation!!.lobbyTimer = ( + dateTime.timeInMillis - (dateTime.time.seconds * DateConstants.SECOND_DIVIDER) + ) / DateConstants.SECOND_DIVIDER + } else if (!isChecked) { + conversation!!.lobbyTimer = 0 + } + + conversation!!.lobbyState = if (isChecked) { + ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY + } else { + ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS + } + + if ( + conversation!!.lobbyTimer != null && + conversation!!.lobbyTimer != Long.MIN_VALUE && + conversation!!.lobbyTimer != 0L + ) { + binding.webinarInfoView.startTimeButtonSummary.text = ( + dateUtils.getLocalDateTimeStringFromTimestamp( + conversation!!.lobbyTimer * DateConstants.SECOND_DIVIDER + ) + ) + } else { + binding.webinarInfoView.startTimeButtonSummary.setText(R.string.nc_manual) + } + + if (isChecked) { + binding.webinarInfoView.startTimeButton.visibility = VISIBLE + } else { + binding.webinarInfoView.startTimeButton.visibility = GONE + } + } + + private fun submitLobbyChanges() { + val state = if (binding.webinarInfoView.lobbySwitch.isChecked) { + 1 + } else { + 0 + } + + val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1)) + + ncApi.setLobbyForConversation( + ApiUtils.getCredentials(conversationUser.username, conversationUser.token), + ApiUtils.getUrlForRoomWebinaryLobby(apiVersion, conversationUser.baseUrl!!, conversation!!.token), + state, + conversation!!.lobbyTimer + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onComplete() { + // unused atm + } + + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(t: GenericOverall) { + // unused atm + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to setLobbyForConversation", e) + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + }) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(eventStatus: EventStatus) { + getListOfParticipants() + } + + private fun showDeleteConversationDialog() { + binding.conversationInfoName.let { + val dialogBuilder = MaterialAlertDialogBuilder(it.context) + .setIcon( + viewThemeUtils.dialog.colorMaterialAlertDialogIcon( + context, + R.drawable.ic_delete_black_24dp + ) + ) + .setTitle(R.string.nc_delete_call) + .setMessage(R.string.nc_delete_conversation_more) + .setPositiveButton(R.string.nc_delete) { _, _ -> + deleteConversation() + } + .setNegativeButton(R.string.nc_cancel) { _, _ -> + // unused atm + } + + viewThemeUtils.dialog + .colorMaterialAlertDialogBackground(it.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } + + private fun setupAdapter() { + if (adapter == null) { + adapter = FlexibleAdapter(userItems, this, true) + } + + val layoutManager = SmoothScrollLinearLayoutManager(this) + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.setHasFixedSize(false) + binding.recyclerView.adapter = adapter + binding.recyclerView.isNestedScrollingEnabled = false + adapter!!.addListener(this) + } + + private fun handleParticipants(participants: List) { + var userItem: ParticipantItem + var participant: Participant + + userItems = ArrayList() + var ownUserItem: ParticipantItem? = null + + for (i in participants.indices) { + participant = participants[i] + userItem = ParticipantItem(this, participant, conversationUser, viewThemeUtils, conversation!!) + if (participant.sessionId != null) { + userItem.isOnline = !participant.sessionId.equals("0") + } else { + userItem.isOnline = participant.sessionIds.isNotEmpty() + } + + if (participant.calculatedActorType == USERS && + participant.calculatedActorId == conversationUser.userId + ) { + ownUserItem = userItem + ownUserItem.model.sessionId = "-1" + ownUserItem.isOnline = true + } else { + userItems.add(userItem) + } + } + + Collections.sort(userItems, ParticipantItemComparator()) + + if (ownUserItem != null) { + userItems.add(0, ownUserItem) + } + + setupAdapter() + + binding.participants.visibility = VISIBLE + adapter!!.updateDataSet(userItems) + } + + private fun getListOfParticipants() { + // FIXME Fix API checking with guests? + val apiVersion: Int = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1)) + + val fieldMap = HashMap() + fieldMap["includeStatus"] = true + + ncApi.getPeersForCall( + credentials, + ApiUtils.getUrlForParticipants( + apiVersion, + conversationUser.baseUrl!!, + conversationToken + ), + fieldMap + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + participantsDisposable = d + } + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onNext(participantsOverall: ParticipantsOverall) { + handleParticipants(participantsOverall.ocs!!.data!!) + } + + override fun onError(e: Throwable) { + // unused atm + } + + override fun onComplete() { + participantsDisposable!!.dispose() + } + }) + } + + private fun listBans() { + val fragmentManager = supportFragmentManager + val newFragment = DialogBanListFragment(conversationToken) + val transaction = fragmentManager.beginTransaction() + transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + transaction + .add(android.R.id.content, newFragment) + .addToBackStack(null) + .commit() + } + + private fun executeIfResultOk(result: ActivityResult, onResult: (intent: Intent?) -> Unit) { + if (result.resultCode == RESULT_OK) { + onResult(result.data) + } else { + Log.e(ChatActivity.TAG, "resultCode for received intent was != ok") + } + } + + private fun selectParticipantsToAdd() { + val bundle = Bundle() + val existingParticipants = ArrayList() + for (userItem in userItems) { + val user = AutocompleteUser( + userItem.model.calculatedActorId!!, + userItem.model.displayName, + userItem.model.calculatedActorType.name.lowercase() + ) + existingParticipants.add(user) + } + + bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true) + bundle.putParcelableArrayList("selectedParticipants", existingParticipants) + bundle.putBoolean(KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS, true) + bundle.putString(BundleKeys.KEY_TOKEN, conversation!!.token) + + val intent = Intent(this, ContactsActivity::class.java) + intent.putExtras(bundle) + + addParticipantsResult.launch(intent) + } + + private fun addParticipantsToConversation(autocompleteUsers: List) { + val groupIdsArray: MutableSet = HashSet() + val emailIdsArray: MutableSet = HashSet() + val circleIdsArray: MutableSet = HashSet() + val userIdsArray: MutableSet = HashSet() + + autocompleteUsers.forEach { participant -> + when (participant.source) { + GROUPS.name.lowercase() -> groupIdsArray.add(participant.id!!) + Participant.ActorType.EMAILS.name.lowercase() -> emailIdsArray.add(participant.id!!) + CIRCLES.name.lowercase() -> circleIdsArray.add(participant.id!!) + else -> userIdsArray.add(participant.id!!) + } + } + + val data = Data.Builder() + data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!) + data.putString(BundleKeys.KEY_TOKEN, conversationToken) + data.putStringArray(BundleKeys.KEY_SELECTED_USERS, userIdsArray.toTypedArray()) + data.putStringArray(BundleKeys.KEY_SELECTED_GROUPS, groupIdsArray.toTypedArray()) + data.putStringArray(BundleKeys.KEY_SELECTED_EMAILS, emailIdsArray.toTypedArray()) + data.putStringArray(BundleKeys.KEY_SELECTED_CIRCLES, circleIdsArray.toTypedArray()) + val addParticipantsToConversationWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder( + AddParticipantsToConversationWorker::class.java + ).setInputData(data.build()).build() + WorkManager.getInstance().enqueue(addParticipantsToConversationWorker) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(addParticipantsToConversationWorker.id) + .observeForever { workInfo: WorkInfo? -> + if (workInfo != null) { + when (workInfo.state) { + WorkInfo.State.RUNNING -> { + Log.d(TAG, "running AddParticipantsToConversation") + } + + WorkInfo.State.SUCCEEDED -> { + Log.d(TAG, "success AddParticipantsToConversation") + getListOfParticipants() + } + + WorkInfo.State.FAILED -> { + Log.d(TAG, "failed AddParticipantsToConversation") + } + + else -> { + } + } + } + } + } + + private fun leaveConversation() { + workerData?.let { data -> + val workRequest = OneTimeWorkRequest.Builder(LeaveConversationWorker::class.java) + .setInputData(data) + .build() + + WorkManager.getInstance(context) + .enqueueUniqueWork( + "leave_conversation_work", + ExistingWorkPolicy.REPLACE, + workRequest + ) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(workRequest.id) + .observe(this, { workInfo: WorkInfo? -> + if (workInfo != null) { + when (workInfo.state) { + WorkInfo.State.SUCCEEDED -> { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + + WorkInfo.State.FAILED -> { + val errorType = workInfo.outputData.getString("error_type") + if (errorType == LeaveConversationWorker.ERROR_NO_OTHER_MODERATORS_OR_OWNERS_LEFT) { + Snackbar.make( + binding.root, + R.string.nc_last_moderator_leaving_room_warning, + Snackbar.LENGTH_LONG + ).show() + } else { + Snackbar.make( + binding.root, + R.string.nc_common_error_sorry, + Snackbar.LENGTH_LONG + ).show() + } + } + + else -> { + } + } + } + }) + } + } + + private fun showClearHistoryDialog() { + binding.conversationInfoName.context.let { + val dialogBuilder = MaterialAlertDialogBuilder(it) + .setIcon( + viewThemeUtils.dialog.colorMaterialAlertDialogIcon( + context, + R.drawable.ic_delete_black_24dp + ) + ) + .setTitle(R.string.nc_clear_history) + .setMessage(R.string.nc_clear_history_warning) + .setPositiveButton(R.string.nc_delete_all) { _, _ -> + clearHistory() + } + .setNegativeButton(R.string.nc_cancel) { _, _ -> + // unused atm + } + + viewThemeUtils.dialog + .colorMaterialAlertDialogBackground(it, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } + + private fun clearHistory() { + val apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1)) + viewModel.clearChatHistory(apiVersion, conversationToken) + } + + private fun deleteConversation() { + workerData?.let { + WorkManager.getInstance(context).enqueue( + OneTimeWorkRequest.Builder( + DeleteConversationWorker::class.java + ).setInputData(it).build() + ) + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + } + + @Suppress("LongMethod") + private fun handleConversation() { + val conversationCopy = conversation ?: return + setUpNotificationSettings(databaseStorageModule!!) + + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RICH_OBJECT_LIST_MEDIA) && + conversationCopy.remoteServer.isNullOrEmpty() + ) { + binding.sharedItemsButton.setOnClickListener { showSharedItems() } + } else { + binding.sharedItemsButton.visibility = GONE + binding.sharedItems.visibility = GONE + } + + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.THREADS)) { + binding.sharedItems.visibility = VISIBLE + binding.showThreadsButton.setOnClickListener { openThreadsOverview() } + } else { + binding.showThreadsButton.visibility = GONE + } + + if (conversation!!.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.CONVERSATION_CREATION_ALL) + ) { + binding.addParticipantsAction.visibility = GONE + binding.startGroupChat.visibility = VISIBLE + showDeleteAllMessagesOption(conversationCopy) + } else if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) { + binding.addParticipantsAction.visibility = VISIBLE + showDeleteAllMessagesOption(conversationCopy) + showOptionsMenu() + } else { + binding.addParticipantsAction.visibility = GONE + if (ConversationUtils.isNoteToSelfConversation(conversation)) { + binding.notificationSettingsView.notificationSettings.visibility = VISIBLE + } else { + binding.clearConversationHistory.visibility = GONE + } + } + + binding.notificationSettingsView.importantConversationSwitch.isChecked = conversation!!.hasImportant + + binding.notificationSettingsView.notificationSettingsImportantConversation.setOnClickListener { + val isChecked = binding.notificationSettingsView.importantConversationSwitch.isChecked + binding.notificationSettingsView.importantConversationSwitch.isChecked = !isChecked + if (!isChecked) { + viewModel.markConversationAsImportant( + credentials, + conversationUser.baseUrl!!, + conversation?.token!! + ) + } else { + viewModel.markConversationAsUnimportant( + credentials, + conversationUser.baseUrl!!, + conversation?.token!! + ) + } + } + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.IMPORTANT_CONVERSATIONS)) { + binding.notificationSettingsView.notificationSettingsImportantConversation.visibility = VISIBLE + } else { + binding.notificationSettingsView.notificationSettingsImportantConversation.visibility = GONE + } + + if (!hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.ARCHIVE_CONVERSATIONS)) { + binding.archiveConversationBtn.visibility = GONE + binding.archiveConversationTextHint.visibility = GONE + } + + binding.archiveConversationBtn.setOnClickListener { + this.lifecycleScope.launch { + if (conversation!!.hasArchived) { + viewModel.unarchiveConversation(conversationUser, conversationToken) + binding.archiveConversationIcon + .setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.outline_archive_24, null)) + binding.archiveConversationText.text = resources.getString(R.string.archive_conversation) + binding.archiveConversationTextHint.text = resources.getString(R.string.archive_hint) + } else { + viewModel.archiveConversation(conversationUser, conversationToken) + binding.archiveConversationIcon + .setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_eye, null)) + binding.archiveConversationText.text = resources.getString(R.string.unarchive_conversation) + binding.archiveConversationTextHint.text = resources.getString(R.string.unarchive_hint) + } + } + viewModel.getRoom(conversationUser, conversationToken) + } + + if (conversation!!.hasArchived) { + binding.archiveConversationIcon + .setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_eye, null)) + binding.archiveConversationText.text = resources.getString(R.string.unarchive_conversation) + binding.archiveConversationTextHint.text = resources.getString(R.string.unarchive_hint) + } else { + binding.archiveConversationIcon + .setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.outline_archive_24, null)) + binding.archiveConversationText.text = resources.getString(R.string.archive_conversation) + binding.archiveConversationTextHint.text = resources.getString(R.string.archive_hint) + } + + binding.notificationSettingsView.sensitiveConversationSwitch.isChecked = conversation!!.hasSensitive + + binding.notificationSettingsView.notificationSettingsSensitiveConversation.setOnClickListener { + val isChecked = !binding.notificationSettingsView.sensitiveConversationSwitch.isChecked + binding.notificationSettingsView.sensitiveConversationSwitch.isChecked = isChecked + if (isChecked) { + viewModel.markConversationAsSensitive( + credentials, + conversationUser.baseUrl!!, + conversation?.token!! + ) + } else { + viewModel.markConversationAsInsensitive( + credentials, + conversationUser.baseUrl!!, + conversation?.token!! + ) + } + } + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SENSITIVE_CONVERSATIONS)) { + binding.notificationSettingsView.notificationSettingsSensitiveConversation.visibility = VISIBLE + } else { + binding.notificationSettingsView.notificationSettingsSensitiveConversation.visibility = GONE + } + if (ConversationUtils.isConversationReadOnlyAvailable(conversationCopy, spreedCapabilities)) { + binding.lockConversation.visibility = VISIBLE + binding.lockConversationSwitch.isChecked = databaseStorageModule!!.getBoolean("lock_switch", false) + + binding.lockConversation.setOnClickListener { + val isLocked = binding.lockConversationSwitch.isChecked + binding.lockConversationSwitch.isChecked = !isLocked + lifecycleScope.launch { + databaseStorageModule!!.saveBoolean("lock_switch", !isLocked) + } + val state = if (isLocked) 0 else 1 + makeConversationReadOnly(conversationToken, state) + } + } else { + binding.lockConversation.visibility = GONE + } + + if (!isDestroyed) { + binding.dangerZoneOptions.visibility = VISIBLE + + setupWebinaryView() + + if (!conversation!!.canLeaveConversation) { + binding.leaveConversationAction.visibility = GONE + } else { + binding.leaveConversationAction.visibility = VISIBLE + } + + if (!conversation!!.canDeleteConversation) { + binding.deleteConversationAction.visibility = GONE + } else { + binding.deleteConversationAction.visibility = VISIBLE + } + + if (ConversationEnums.ConversationType.ROOM_SYSTEM == conversation!!.type) { + binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE + } + + binding.listBansButton.visibility = + if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities) && + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != conversation!!.type + ) { + VISIBLE + } else { + GONE + } + + if (conversation!!.notificationCalls === null) { + binding.notificationSettingsView.callNotificationsSwitch.visibility = GONE + } else { + binding.notificationSettingsView.callNotificationsSwitch.isChecked = + (conversationCopy.notificationCalls == 1) + } + + getListOfParticipants() + + binding.progressBar.visibility = GONE + + binding.conversationInfoName.visibility = VISIBLE + + binding.displayNameText.text = conversation!!.displayName + + if (conversation!!.description != null && conversation!!.description.isNotEmpty()) { + binding.descriptionText.text = conversation!!.description + binding.conversationDescription.visibility = VISIBLE + } + + loadConversationAvatar() + adjustNotificationLevelUI() + initRecordingConsentOption() + initExpiringMessageOption() + + binding.let { + GuestAccessHelper( + this@ConversationInfoActivity, + it, + conversation!!, + spreedCapabilities, + conversationUser, + viewModel, + this + ).setupGuestAccess() + } + if (ConversationUtils.isNoteToSelfConversation(conversation!!)) { + binding.notificationSettingsView.notificationSettings.visibility = GONE + } else { + binding.notificationSettingsView.notificationSettings.visibility = VISIBLE + } + } + } + + private fun makeConversationReadOnly(roomToken: String, state: Int) { + viewModel.setConversationReadOnly(roomToken, state) + } + + private fun initRecordingConsentOption() { + fun hide() { + binding.recordingConsentView.recordingConsentSettingsCategory.visibility = GONE + binding.recordingConsentView.recordingConsentForConversation.visibility = GONE + binding.recordingConsentView.recordingConsentAll.visibility = GONE + } + + fun showAlwaysRequiredInfo() { + binding.recordingConsentView.recordingConsentForConversation.visibility = GONE + binding.recordingConsentView.recordingConsentAll.visibility = VISIBLE + } + + fun showSwitch() { + binding.recordingConsentView.recordingConsentForConversation.visibility = VISIBLE + binding.recordingConsentView.recordingConsentAll.visibility = GONE + + if (conversation!!.hasCall) { + binding.recordingConsentView.recordingConsentForConversation.isEnabled = false + binding.recordingConsentView.recordingConsentForConversation.alpha = LOW_EMPHASIS_OPACITY + } else { + binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked = + conversation!!.recordingConsentRequired == RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION + + binding.recordingConsentView.recordingConsentForConversation.setOnClickListener { + binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked = + !binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked + submitRecordingConsentChanges() + } + } + } + + if (ConversationUtils.isParticipantOwnerOrModerator(conversation!!) && + !ConversationUtils.isNoteToSelfConversation(conversation!!) + ) { + when (CapabilitiesUtil.getRecordingConsentType(spreedCapabilities)) { + CapabilitiesUtil.RECORDING_CONSENT_NOT_REQUIRED -> hide() + CapabilitiesUtil.RECORDING_CONSENT_REQUIRED -> showAlwaysRequiredInfo() + CapabilitiesUtil.RECORDING_CONSENT_DEPEND_ON_CONVERSATION -> showSwitch() + } + } else { + hide() + } + } + + fun showDeleteAllMessagesOption(conversationCopy: ConversationModel) { + if (hasSpreedFeatureCapability( + spreedCapabilities, + SpreedFeatures.CLEAR_HISTORY + ) && + conversationCopy.canDeleteConversation + ) { + binding.clearConversationHistory.visibility = VISIBLE + } else { + binding.clearConversationHistory.visibility = GONE + } + } + + private fun submitRecordingConsentChanges() { + val state = if (binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked) { + RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION + } else { + RECORDING_CONSENT_NOT_REQUIRED_FOR_CONVERSATION + } + + val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1)) + + ncApi.setRecordingConsent( + ApiUtils.getCredentials(conversationUser.username, conversationUser.token), + ApiUtils.getUrlForRecordingConsent(apiVersion, conversationUser.baseUrl!!, conversation!!.token), + state + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onComplete() { + // unused atm + } + + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(t: GenericOverall) { + // unused atm + } + + override fun onError(e: Throwable) { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + Log.e(TAG, "Error when setting recording consent option for conversation", e) + } + }) + } + + private fun initExpiringMessageOption() { + if (ConversationUtils.isParticipantOwnerOrModerator(conversation!!) && + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION) + ) { + databaseStorageModule?.setMessageExpiration(conversation!!.messageExpiration) + val value = databaseStorageModule!!.getString("conversation_settings_dropdown", "") + val pos = resources.getStringArray(R.array.message_expiring_values).indexOf(value) + val text = resources.getStringArray(R.array.message_expiring_descriptions)[pos] + binding.conversationSettingsDropdown.setText(text) + binding.conversationSettingsDropdown + .setSimpleItems(resources.getStringArray(R.array.message_expiring_descriptions)) + binding.conversationSettingsDropdown.setOnItemClickListener { _, _, position, _ -> + val v: String = resources.getStringArray(R.array.message_expiring_values)[position] + lifecycleScope.launch { + databaseStorageModule!!.saveString("conversation_settings_dropdown", v) + } + } + binding.messageExpirationSettings.visibility = VISIBLE + } else { + binding.messageExpirationSettings.visibility = GONE + } + } + + private fun adjustNotificationLevelUI() { + if (conversation != null) { + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.NOTIFICATION_LEVELS)) { + binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.isEnabled = true + binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.alpha = 1.0f + + if (conversation!!.notificationLevel != ConversationEnums.NotificationLevel.DEFAULT) { + val stringValue: String = + when ( + DomainEnumNotificationLevelConverter() + .convertToInt(conversation!!.notificationLevel) + ) { + NOTIFICATION_LEVEL_ALWAYS -> resources.getString(R.string.nc_notify_me_always) + NOTIFICATION_LEVEL_MENTION -> resources.getString(R.string.nc_notify_me_mention) + NOTIFICATION_LEVEL_NEVER -> resources.getString(R.string.nc_notify_me_never) + else -> resources.getString(R.string.nc_notify_me_mention) + } + + binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.setText( + stringValue + ) + } else { + setProperNotificationValue(conversation) + } + } else { + binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.isEnabled = false + binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.alpha = + LOW_EMPHASIS_OPACITY + setProperNotificationValue(conversation) + } + binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown + .setSimpleItems(resources.getStringArray(R.array.message_notification_levels)) + } + } + + private fun setProperNotificationValue(conversation: ConversationModel?) { + if (conversation!!.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) { + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)) { + binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.setText( + resources.getString(R.string.nc_notify_me_always) + ) + } else { + binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.setText( + resources.getString(R.string.nc_notify_me_mention) + ) + } + } else { + binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown.setText( + resources.getString(R.string.nc_notify_me_mention) + ) + } + } + + private fun loadConversationAvatar() { + when (conversation!!.type) { + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty( + conversation!!.name + ) + ) { + conversation!!.name.let { + binding.avatarImage.loadUserAvatar( + conversationUser, + it, + true, + false + ) + } + } + + ConversationEnums.ConversationType.ROOM_GROUP_CALL, ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> { + binding.avatarImage.loadConversationAvatar( + conversationUser, + conversation!!, + false, + viewThemeUtils + ) + } + + ConversationEnums.ConversationType.ROOM_SYSTEM -> { + binding.avatarImage.loadSystemAvatar() + } + + ConversationEnums.ConversationType.NOTE_TO_SELF -> { + binding.avatarImage.loadNoteToSelfAvatar() + } + + else -> { + // unused atm + } + } + } + + private fun toggleModeratorStatus(apiVersion: Int, participant: Participant) { + val subscriber = object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + getListOfParticipants() + } + + @SuppressLint("LongLogTag") + override fun onError(e: Throwable) { + Log.e(TAG, "Error toggling moderator status", e) + } + + override fun onComplete() { + // unused atm + } + } + + if (participant.type == Participant.ParticipantType.MODERATOR || + participant.type == Participant.ParticipantType.GUEST_MODERATOR + ) { + ncApi.demoteAttendeeFromModerator( + credentials, + ApiUtils.getUrlForRoomModerators( + apiVersion, + conversationUser.baseUrl!!, + conversation!!.token + ), + participant.attendeeId + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(subscriber) + } else if (participant.type == Participant.ParticipantType.USER || + participant.type == Participant.ParticipantType.GUEST + ) { + ncApi.promoteAttendeeToModerator( + credentials, + ApiUtils.getUrlForRoomModerators( + apiVersion, + conversationUser.baseUrl!!, + conversation!!.token + ), + participant.attendeeId + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(subscriber) + } + } + + private fun toggleModeratorStatusLegacy(apiVersion: Int, participant: Participant) { + val subscriber = object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + getListOfParticipants() + } + + @SuppressLint("LongLogTag") + override fun onError(e: Throwable) { + Log.e(TAG, "Error toggling moderator status (legacy)", e) + } + + override fun onComplete() { + // unused atm + } + } + + if (participant.type == Participant.ParticipantType.MODERATOR) { + ncApi.demoteModeratorToUser( + credentials, + ApiUtils.getUrlForRoomModerators( + apiVersion, + conversationUser.baseUrl!!, + conversation!!.token + ), + participant.userId + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(subscriber) + } else if (participant.type == Participant.ParticipantType.USER) { + ncApi.promoteUserToModerator( + credentials, + ApiUtils.getUrlForRoomModerators( + apiVersion, + conversationUser.baseUrl!!, + conversation!!.token + ), + participant.userId + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(subscriber) + } + } + + private fun banActor(actorType: String, actorId: String, internalNote: String) { + viewModel.banActor(conversationUser, conversationToken, actorType, actorId, internalNote) + } + + private fun removeAttendeeFromConversation(apiVersion: Int, participant: Participant) { + if (apiVersion >= ApiUtils.API_V4) { + ncApi.removeAttendeeFromConversation( + credentials, + ApiUtils.getUrlForAttendees( + apiVersion, + conversationUser.baseUrl!!, + conversation!!.token + ), + participant.attendeeId + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + getListOfParticipants() + } + + @SuppressLint("LongLogTag") + override fun onError(e: Throwable) { + Log.e(TAG, "Error removing attendee from conversation", e) + } + + override fun onComplete() { + // unused atm + } + }) + } else { + if (participant.type == Participant.ParticipantType.GUEST || + participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK + ) { + ncApi.removeParticipantFromConversation( + credentials, + ApiUtils.getUrlForRemovingParticipantFromConversation( + conversationUser.baseUrl!!, + conversation!!.token, + true + ), + participant.sessionId + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + getListOfParticipants() + } + + @SuppressLint("LongLogTag") + override fun onError(e: Throwable) { + Log.e(TAG, "Error removing guest from conversation", e) + } + + override fun onComplete() { + // unused atm + } + }) + } else { + ncApi.removeParticipantFromConversation( + credentials, + ApiUtils.getUrlForRemovingParticipantFromConversation( + conversationUser.baseUrl!!, + conversation!!.token, + false + ), + participant.userId + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + getListOfParticipants() + } + + @SuppressLint("LongLogTag") + override fun onError(e: Throwable) { + Log.e(TAG, "Error removing user from conversation", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + } + } + + @SuppressLint("CheckResult", "StringFormatInvalid") + override fun onItemClick(view: View?, position: Int): Boolean { + if (!ConversationUtils.canModerate(conversation!!, spreedCapabilities)) { + return true + } + + val userItem = adapter?.getItem(position) as ParticipantItem + val participant = userItem.model + val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1)) + + if (participant.calculatedActorType == USERS && participant.calculatedActorId == conversationUser.userId) { + if (participant.attendeePin?.isNotEmpty() == true) { + launchRemoveAttendeeFromConversationDialog( + participant, + apiVersion, + context.getString(R.string.nc_attendee_pin, participant.attendeePin), + R.drawable.ic_lock_grey600_24px + ) + } + } else if (participant.type == Participant.ParticipantType.OWNER) { + // Can not moderate owner + } else if (participant.calculatedActorType == GROUPS) { + launchRemoveAttendeeFromConversationDialog( + participant, + apiVersion, + context.getString(R.string.nc_remove_group_and_members) + ) + } else if (participant.calculatedActorType == CIRCLES) { + launchRemoveAttendeeFromConversationDialog( + participant, + apiVersion, + context.getString(R.string.nc_remove_team_and_members) + ) + } else { + launchDefaultActions(participant, apiVersion) + } + return true + } + + @SuppressLint("CheckResult") + private fun launchDefaultActions(participant: Participant, apiVersion: Int) { + val items = getDefaultActionItems(participant) + + if (CapabilitiesUtil.isBanningAvailable(conversationUser.capabilities?.spreedCapability!!)) { + items.add(BasicListItemWithImage(R.drawable.baseline_block_24, context.getString(R.string.ban_participant))) + } + + when (participant.type) { + Participant.ParticipantType.MODERATOR, Participant.ParticipantType.GUEST_MODERATOR -> { + items.removeAt(1) + } + + Participant.ParticipantType.USER, Participant.ParticipantType.GUEST -> { + items.removeAt(2) + } + + else -> { + // Self joined users can not be promoted nor demoted + items.removeAt(2) + items.removeAt(1) + } + } + + if (participant.attendeePin == null || participant.attendeePin!!.isEmpty()) { + items.removeAt(0) + } + + if (items.isNotEmpty()) { + MaterialDialog(this, BottomSheet(WRAP_CONTENT)).show { + cornerRadius(res = R.dimen.corner_radius) + + title(text = participant.displayName) + listItemsWithImage(items = items) { _, index, _ -> + var actionToTrigger = index + if (participant.attendeePin == null || participant.attendeePin!!.isEmpty()) { + actionToTrigger++ + } + if (participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK) { + actionToTrigger++ + } + + when (actionToTrigger) { + DEMOTE_OR_PROMOTE -> { + if (apiVersion >= ApiUtils.API_V4) { + toggleModeratorStatus(apiVersion, participant) + } else { + toggleModeratorStatusLegacy(apiVersion, participant) + } + } + + REMOVE_FROM_CONVERSATION -> { + removeAttendeeFromConversation(apiVersion, participant) + } + + BAN_FROM_CONVERSATION -> { + handleBan(participant) + } + + else -> {} + } + } + } + } + } + + @SuppressLint("StringFormatInvalid") + private fun getDefaultActionItems(participant: Participant): MutableList { + val items = mutableListOf( + BasicListItemWithImage( + R.drawable.ic_lock_grey600_24px, + context.getString(R.string.nc_attendee_pin, participant.attendeePin) + ), + BasicListItemWithImage( + R.drawable.ic_pencil_grey600_24dp, + context.getString(R.string.nc_promote) + ), + BasicListItemWithImage( + R.drawable.ic_pencil_grey600_24dp, + context.getString(R.string.nc_demote) + ), + BasicListItemWithImage( + R.drawable.ic_delete_grey600_24dp, + context.getString(R.string.nc_remove_participant) + ) + ) + return items + } + + @SuppressLint("CheckResult") + private fun launchRemoveAttendeeFromConversationDialog( + participant: Participant, + apiVersion: Int, + itemText: String, + @DrawableRes itemIcon: Int = R.drawable.ic_delete_grey600_24dp + ) { + val items = mutableListOf(BasicListItemWithImage(itemIcon, itemText)) + MaterialDialog(this, BottomSheet(WRAP_CONTENT)).show { + cornerRadius(res = R.dimen.corner_radius) + + title(text = participant.displayName) + listItemsWithImage(items = items) { _, index, _ -> + if (index == 0) { + removeAttendeeFromConversation(apiVersion, participant) + } + } + } + } + + private fun MaterialDialog.handleBan(participant: Participant) { + val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1)) + val binding = DialogBanParticipantBinding.inflate(layoutInflater) + val actorTypeConverter = EnumActorTypeConverter() + val dialog = MaterialAlertDialogBuilder(context).setView(binding.root).create() + binding.avatarImage.loadUserAvatar( + conversationUser, + participant.actorId!!, + requestBigSize = true, + ignoreCache = false + ) + binding.displayNameText.text = participant.actorId + binding.buttonBan.setOnClickListener { + banActor( + actorTypeConverter.convertToString(participant.actorType!!), + participant.actorId!!, + binding.banParticipantEdit.text.toString() + ) + removeAttendeeFromConversation(apiVersion, participant) + dialog.dismiss() + } + binding.buttonClose.setOnClickListener { dialog.dismiss() } + viewThemeUtils.material.colorTextInputLayout(binding.banParticipantEditLayout) + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.buttonBan) + viewThemeUtils.material.colorMaterialButtonText(binding.buttonClose) + dialog.show() + } + + private fun setUpNotificationSettings(module: DatabaseStorageModule) { + binding.notificationSettingsView.notificationSettingsCallNotifications.setOnClickListener { + val isChecked = binding.notificationSettingsView.callNotificationsSwitch.isChecked + binding.notificationSettingsView.callNotificationsSwitch.isChecked = !isChecked + lifecycleScope.launch { + module.saveBoolean("call_notifications_switch", !isChecked) + } + } + + binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown + .setOnItemClickListener { _, _, position, _ -> + val value = resources.getStringArray(R.array.message_notification_levels_entry_values)[position] + Log.i(TAG, "saved $value to module from $position") + lifecycleScope.launch { + module.saveString("conversation_info_message_notifications_dropdown", value) + } + } + + if (conversation!!.remoteServer.isNullOrEmpty()) { + binding.notificationSettingsView.notificationSettingsCallNotifications.visibility = VISIBLE + binding.notificationSettingsView.callNotificationsSwitch.isChecked = module + .getBoolean("call_notifications_switch", true) + } else { + binding.notificationSettingsView.notificationSettingsCallNotifications.visibility = GONE + } + } + + companion object { + private val TAG = ConversationInfoActivity::class.java.simpleName + private const val NOTIFICATION_LEVEL_ALWAYS: Int = 1 + private const val NOTIFICATION_LEVEL_MENTION: Int = 2 + private const val NOTIFICATION_LEVEL_NEVER: Int = 3 + private const val LOW_EMPHASIS_OPACITY: Float = 0.38f + private const val RECORDING_CONSENT_NOT_REQUIRED_FOR_CONVERSATION: Int = 0 + private const val RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION: Int = 1 + private const val DEMOTE_OR_PROMOTE = 1 + private const val REMOVE_FROM_CONVERSATION = 2 + private const val BAN_FROM_CONVERSATION = 3 + } + + /** + * Comparator for participants, sorts by online-status, moderator-status and display name. + */ + class ParticipantItemComparator : Comparator { + override fun compare(left: ParticipantItem, right: ParticipantItem): Int { + val leftIsGroup = left.model.actorType == GROUPS || left.model.actorType == CIRCLES + val rightIsGroup = right.model.actorType == GROUPS || right.model.actorType == CIRCLES + if (leftIsGroup != rightIsGroup) { + // Groups below participants + return if (rightIsGroup) { + -1 + } else { + 1 + } + } + + if (left.isOnline && !right.isOnline) { + return -1 + } else if (!left.isOnline && right.isOnline) { + return 1 + } + + val moderatorTypes = ArrayList() + moderatorTypes.add(Participant.ParticipantType.MODERATOR) + moderatorTypes.add(Participant.ParticipantType.OWNER) + moderatorTypes.add(Participant.ParticipantType.GUEST_MODERATOR) + + if (moderatorTypes.contains(left.model.type) && !moderatorTypes.contains(right.model.type)) { + return -1 + } else if (!moderatorTypes.contains(left.model.type) && moderatorTypes.contains(right.model.type)) { + return 1 + } + + return left.model.displayName!!.lowercase(Locale.ROOT).compareTo( + right.model.displayName!!.lowercase(Locale.ROOT) + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/CreateRoomRequest.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/CreateRoomRequest.kt new file mode 100644 index 0000000..9aa8b6b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/CreateRoomRequest.kt @@ -0,0 +1,72 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationinfo + +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject + +@JsonObject +data class CreateRoomRequest( + @JsonField(name = ["roomType"]) + var roomType: String, + @JsonField(name = ["roomName"]) + var roomName: String? = null, + @JsonField(name = ["objectType"]) + var objectType: String? = null, + @JsonField(name = ["objectId"]) + var objectId: String? = null, + @JsonField(name = ["password"]) + var password: String? = null, + @JsonField(name = ["readOnly"]) + var readOnly: Int, + @JsonField(name = ["listable"]) + var listable: Int, + @JsonField(name = ["messageExpiration"]) + var messageExpiration: Int? = null, + @JsonField(name = ["lobbyState"]) + var lobbyState: Int? = null, + @JsonField(name = ["lobbyTimer"]) + var lobbyTimer: Int, + @JsonField(name = ["sipEnabled"]) + var sipEnabled: Int, + @JsonField(name = ["permissions"]) + var permissions: Int, + @JsonField(name = ["recordingConsent"]) + var recordingConsent: Int, + @JsonField(name = ["mentionPermissions"]) + var mentionPermissions: Int, + @JsonField(name = ["description"]) + var description: String? = null, + @JsonField(name = ["emoji"]) + var emoji: String? = null, + @JsonField(name = ["avatarColor"]) + var avatarColor: String? = null, + @JsonField(name = ["participants"]) + var participants: Participants? = null +) { + constructor() : this( + 0.toString(), + "", + "", + "", + "", + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + "", + "", + "", + Participants() + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt new file mode 100644 index 0000000..8756111 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/GuestAccessHelper.kt @@ -0,0 +1,197 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationinfo + +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.LifecycleOwner +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityConversationInfoBinding +import com.nextcloud.talk.databinding.DialogPasswordBinding +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.repositories.conversations.ConversationsRepository +import com.nextcloud.talk.utils.ConversationUtils +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers + +class GuestAccessHelper( + private val activity: ConversationInfoActivity, + private val binding: ActivityConversationInfoBinding, + private val conversation: ConversationModel, + private val spreedCapabilities: SpreedCapability, + private val conversationUser: User, + private val viewModel: ConversationInfoViewModel, + private val lifecycleOwner: LifecycleOwner +) { + private val conversationsRepository = activity.conversationsRepository + private val viewThemeUtils = activity.viewThemeUtils + private val context = activity.context + + fun setupGuestAccess() { + if (ConversationUtils.canModerate(conversation, spreedCapabilities)) { + binding.guestAccessView.guestAccessSettings.visibility = View.VISIBLE + } else { + binding.guestAccessView.guestAccessSettings.visibility = View.GONE + } + + if (conversation.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL) { + binding.guestAccessView.allowGuestsSwitch.isChecked = true + showAllOptions() + if (conversation.hasPassword) { + binding.guestAccessView.passwordProtectionSwitch.isChecked = true + } + } else { + binding.guestAccessView.allowGuestsSwitch.isChecked = false + hideAllOptions() + } + + binding.guestAccessView.guestAccessSettingsAllowGuest.setOnClickListener { + val isChecked = binding.guestAccessView.allowGuestsSwitch.isChecked + binding.guestAccessView.allowGuestsSwitch.isChecked = !isChecked + viewModel.allowGuests(conversation.token, !isChecked) + viewModel.allowGuestsViewState.observe(lifecycleOwner) { uiState -> + when (uiState) { + is ConversationInfoViewModel.AllowGuestsUIState.Success -> { + binding.guestAccessView.allowGuestsSwitch.isChecked = uiState.allow + if (uiState.allow) { + showAllOptions() + } else { + hideAllOptions() + } + } + is ConversationInfoViewModel.AllowGuestsUIState.Error -> { + val exception = uiState.exception + val message = context.getString(R.string.nc_guest_access_allow_failed) + Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() + Log.e(TAG, message, exception) + } + ConversationInfoViewModel.AllowGuestsUIState.None -> { + } + } + } + } + + binding.guestAccessView.guestAccessSettingsPasswordProtection.setOnClickListener { + val isChecked = binding.guestAccessView.passwordProtectionSwitch.isChecked + binding.guestAccessView.passwordProtectionSwitch.isChecked = !isChecked + if (isChecked) { + viewModel.setPassword("", conversation.token) + passwordObserver() + } else { + showPasswordDialog() + } + } + + binding.guestAccessView.resendInvitationsButton.setOnClickListener { + conversationsRepository.resendInvitations(conversation.token!!).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()).subscribe(ResendInvitationsObserver()) + } + } + + private fun passwordObserver() { + viewModel.passwordViewState.observe(lifecycleOwner) { uiState -> + when (uiState) { + is ConversationInfoViewModel.PasswordUiState.Success -> { + // unused atm + } + is ConversationInfoViewModel.PasswordUiState.Error -> { + val exception = uiState.exception + val message = context.getString(R.string.nc_guest_access_password_failed) + Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() + Log.e(TAG, message, exception) + } + is ConversationInfoViewModel.PasswordUiState.None -> { + // unused atm + } + } + } + } + + private fun showPasswordDialog() { + val builder = MaterialAlertDialogBuilder(activity) + builder.apply { + val dialogPassword = DialogPasswordBinding.inflate(LayoutInflater.from(context)) + viewThemeUtils.platform.colorEditText(dialogPassword.password) + setView(dialogPassword.root) + setTitle(R.string.nc_guest_access_password_dialog_title) + setPositiveButton(R.string.nc_ok) { _, _ -> + val password = dialogPassword.password.text.toString() + viewModel.setPassword(password, conversation.token) + } + setNegativeButton(R.string.nc_cancel) { _, _ -> + binding.guestAccessView.passwordProtectionSwitch.isChecked = false + } + } + createDialog(builder) + passwordObserver() + } + + private fun createDialog(builder: MaterialAlertDialogBuilder) { + builder.create() + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.conversationInfoName.context, builder) + val dialog = builder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + + inner class ResendInvitationsObserver : Observer { + + private lateinit var resendInvitationsResult: ConversationsRepository.ResendInvitationsResult + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(t: ConversationsRepository.ResendInvitationsResult) { + resendInvitationsResult = t + } + + override fun onError(e: Throwable) { + val message = context.getString(R.string.nc_guest_access_resend_invitations_failed) + Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show() + Log.e(TAG, message, e) + } + + override fun onComplete() { + if (resendInvitationsResult.successful) { + Snackbar.make( + binding.root, + R.string.nc_guest_access_resend_invitations_successful, + Snackbar.LENGTH_SHORT + ).show() + } + } + } + + private fun showAllOptions() { + binding.guestAccessView.guestAccessSettingsPasswordProtection.visibility = View.VISIBLE + if (conversationUser.capabilities?.spreedCapability?.features?.contains("sip-support") == true) { + binding.guestAccessView.resendInvitationsButton.visibility = View.VISIBLE + } + } + + private fun hideAllOptions() { + binding.guestAccessView.guestAccessSettingsPasswordProtection.visibility = View.GONE + binding.guestAccessView.resendInvitationsButton.visibility = View.GONE + } + + companion object { + private val TAG = GuestAccessHelper::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/Participants.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/Participants.kt new file mode 100644 index 0000000..1f1e8c8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/Participants.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationinfo + +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject + +@JsonObject +data class Participants( + @JsonField(name = ["users"]) + var users: MutableList = arrayListOf(), + @JsonField(name = ["federated_users"]) + var federatedUsers: MutableList = arrayListOf(), + @JsonField(name = ["groups"]) + var groups: MutableList = arrayListOf(), + @JsonField(name = ["emails"]) + var emails: MutableList = arrayListOf(), + @JsonField(name = ["phones"]) + var phones: MutableList = arrayListOf(), + @JsonField(name = ["teams"]) + var teams: MutableList = arrayListOf() +) diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt new file mode 100644 index 0000000..cb4bc7c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModel.kt @@ -0,0 +1,540 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationinfo.viewmodel + +import android.annotation.SuppressLint +import android.util.Log +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.conversationinfo.CreateRoomRequest +import com.nextcloud.talk.conversationinfo.Participants +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.models.json.participants.Participant.ActorType.CIRCLES +import com.nextcloud.talk.models.json.participants.Participant.ActorType.EMAILS +import com.nextcloud.talk.models.json.participants.Participant.ActorType.FEDERATED +import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS +import com.nextcloud.talk.models.json.participants.TalkBan +import com.nextcloud.talk.models.json.profile.Profile +import com.nextcloud.talk.repositories.conversations.ConversationsRepository +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ApiUtils.getUrlForRooms +import com.nextcloud.talk.utils.DisplayUtils +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ConversationInfoViewModel @Inject constructor( + private val chatNetworkDataSource: ChatNetworkDataSource, + private val conversationsRepository: ConversationsRepository +) : ViewModel() { + + object LifeCycleObserver : DefaultLifecycleObserver { + enum class LifeCycleFlag { + PAUSED, + RESUMED + } + + lateinit var currentLifeCycleFlag: LifeCycleFlag + val disposableSet = mutableSetOf() + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + currentLifeCycleFlag = LifeCycleFlag.RESUMED + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + currentLifeCycleFlag = LifeCycleFlag.PAUSED + disposableSet.forEach { disposable -> disposable.dispose() } + disposableSet.clear() + } + } + + sealed interface ViewState + + class ListBansSuccessState(val talkBans: List) : ViewState + object ListBansErrorState : ViewState + + private val _getTalkBanState: MutableLiveData = MutableLiveData() + val getTalkBanState: LiveData + get() = _getTalkBanState + + class BanActorSuccessState(val talkBan: TalkBan) : ViewState + object BanActorErrorState : ViewState + + private val _getBanActorState: MutableLiveData = MutableLiveData() + val getBanActorState: LiveData + get() = _getBanActorState + + object UnBanActorSuccessState : ViewState + object UnBanActorErrorState : ViewState + + private val _getUnBanActorState: MutableLiveData = MutableLiveData() + val getUnBanActorState: LiveData + get() = _getUnBanActorState + + object GetRoomStartState : ViewState + object GetRoomErrorState : ViewState + open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(GetRoomStartState) + val viewState: LiveData + get() = _viewState + + object GetCapabilitiesStartState : ViewState + object GetCapabilitiesErrorState : ViewState + open class GetCapabilitiesSuccessState(val spreedCapabilities: SpreedCapability) : ViewState + + private val _allowGuestsViewState = MutableLiveData(AllowGuestsUIState.None) + val allowGuestsViewState: LiveData + get() = _allowGuestsViewState + + private val _passwordViewState = MutableLiveData(PasswordUiState.None) + val passwordViewState: LiveData + get() = _passwordViewState + + private val _getCapabilitiesViewState: MutableLiveData = MutableLiveData(GetCapabilitiesStartState) + val getCapabilitiesViewState: LiveData + get() = _getCapabilitiesViewState + + private val _clearChatHistoryViewState: MutableLiveData = + MutableLiveData(ClearChatHistoryViewState.None) + val clearChatHistoryViewState: LiveData + get() = _clearChatHistoryViewState + + private val _getConversationReadOnlyState: MutableLiveData = + MutableLiveData(SetConversationReadOnlyViewState.None) + val getConversationReadOnlyState: LiveData + get() = _getConversationReadOnlyState + + @Suppress("PropertyName") + private val _markConversationAsImportantResult = + MutableLiveData(MarkConversationAsImportantViewState.None) + val markAsImportantResult: LiveData + get() = _markConversationAsImportantResult + + @Suppress("PropertyName") + private val _markConversationAsUnimportantResult = + MutableLiveData(MarkConversationAsUnimportantViewState.None) + val markAsUnimportantResult: LiveData + get() = _markConversationAsUnimportantResult + + private val _createRoomViewState = MutableLiveData(CreateRoomUIState.None) + val createRoomViewState: LiveData + get() = _createRoomViewState + + object GetProfileErrorState : ViewState + class GetProfileSuccessState(val profile: Profile) : ViewState + private val _getProfileViewState = MutableLiveData() + val getProfileViewState: LiveData + get() = _getProfileViewState + + @Suppress("PropertyName") + private val _markConversationAsSensitiveResult = + MutableLiveData(MarkConversationAsSensitiveViewState.None) + val markAsSensitiveResult: LiveData + get() = _markConversationAsSensitiveResult + + @Suppress("PropertyName") + private val _markConversationAsInsensitiveResult = + MutableLiveData(MarkConversationAsInsensitiveViewState.None) + val markAsInsensitiveResult: LiveData + get() = _markConversationAsInsensitiveResult + + fun getRoom(user: User, token: String) { + _viewState.value = GetRoomStartState + chatNetworkDataSource.getRoom(user, token) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(GetRoomObserver()) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun createRoomFromOneToOne( + user: User, + userItems: List, + autocompleteUsers: List, + roomToken: String + ) { + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, 1)) + val url = getUrlForRooms(apiVersion, user.baseUrl!!) + val credentials = ApiUtils.getCredentials(user.username, user.token)!! + + val participantsBody = convertAutocompleteUserToParticipant(autocompleteUsers) + + val body = CreateRoomRequest( + roomName = createConversationNameByParticipants( + userItems.map { it.displayName }, + autocompleteUsers.map { it.label } + ), + roomType = GROUP_CONVERSATION_TYPE, + readOnly = 0, + listable = 1, + lobbyTimer = 0, + sipEnabled = 0, + permissions = 0, + recordingConsent = 0, + mentionPermissions = 0, + participants = participantsBody, + objectType = EXTENDED_CONVERSATION, + objectId = roomToken + ) + + viewModelScope.launch { + try { + val roomOverall = conversationsRepository.createRoom( + credentials, + url, + body + ) + _createRoomViewState.value = CreateRoomUIState.Success(roomOverall) + } catch (e: Exception) { + Log.e(TAG, "Failed to create room", e) + _createRoomViewState.value = CreateRoomUIState.Error(e) + } + } + } + + private fun convertAutocompleteUserToParticipant(autocompleteUsers: List): Participants { + val participants = Participants() + + autocompleteUsers.forEach { autocompleteUser -> + when (autocompleteUser.source) { + GROUPS.name.lowercase() -> participants.groups.add(autocompleteUser.id!!) + EMAILS.name.lowercase() -> participants.emails.add(autocompleteUser.id!!) + CIRCLES.name.lowercase() -> participants.teams.add(autocompleteUser.id!!) + FEDERATED.name.lowercase() -> participants.federatedUsers.add(autocompleteUser.id!!) + "phones".lowercase() -> participants.phones.add(autocompleteUser.id!!) + else -> participants.users.add(autocompleteUser.id!!) + } + } + + return participants + } + + fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) { + _getCapabilitiesViewState.value = GetCapabilitiesStartState + + if (conversationModel.remoteServer.isNullOrEmpty()) { + _getCapabilitiesViewState.value = GetCapabilitiesSuccessState(user.capabilities!!.spreedCapability!!) + } else { + chatNetworkDataSource.getCapabilities(user, token) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + LifeCycleObserver.disposableSet.add(d) + } + + override fun onNext(spreedCapabilities: SpreedCapability) { + _getCapabilitiesViewState.value = GetCapabilitiesSuccessState(spreedCapabilities) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching spreed capabilities", e) + _getCapabilitiesViewState.value = GetCapabilitiesErrorState + } + + override fun onComplete() { + // unused atm + } + }) + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun listBans(user: User, token: String) { + val url = ApiUtils.getUrlForBans(user.baseUrl!!, token) + viewModelScope.launch { + try { + val listBans = conversationsRepository.listBans(user.getCredentials(), url) + _getTalkBanState.value = ListBansSuccessState(listBans) + } catch (exception: Exception) { + _getTalkBanState.value = ListBansErrorState + Log.e(TAG, "Error while getting list of banned participants", exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun banActor(user: User, token: String, actorType: String, actorId: String, internalNote: String) { + val url = ApiUtils.getUrlForBans(user.baseUrl!!, token) + viewModelScope.launch { + try { + val talkBan = conversationsRepository.banActor( + user.getCredentials(), + url, + actorType, + actorId, + internalNote + ) + _getBanActorState.value = BanActorSuccessState(talkBan) + } catch (exception: Exception) { + _getBanActorState.value = BanActorErrorState + Log.e(TAG, "Error banning a participant", exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun setConversationReadOnly(roomToken: String, state: Int) { + viewModelScope.launch { + try { + conversationsRepository.setConversationReadOnly(roomToken, state) + _getConversationReadOnlyState.value = SetConversationReadOnlyViewState.Success + } catch (exception: Exception) { + _getConversationReadOnlyState.value = SetConversationReadOnlyViewState.Error(exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun unbanActor(user: User, token: String, banId: Int) { + val url = ApiUtils.getUrlForUnban(user.baseUrl!!, token, banId) + viewModelScope.launch { + try { + conversationsRepository.unbanActor(user.getCredentials(), url) + _getUnBanActorState.value = UnBanActorSuccessState + } catch (exception: Exception) { + _getUnBanActorState.value = UnBanActorErrorState + Log.e(TAG, "Error while unbanning a participant", exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun getProfileData(user: User, userId: String) { + val url = ApiUtils.getUrlForProfile(user.baseUrl!!, userId) + viewModelScope.launch { + try { + val profile = conversationsRepository.getProfile(user.getCredentials(), url) + if (profile != null) { + _getProfileViewState.value = GetProfileSuccessState(profile) + } else { + _getProfileViewState.value = GetProfileErrorState + } + } catch (e: Exception) { + Log.w(TAG, "Failed to get profile data (if not supported there wil be http405)", e) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun allowGuests(token: String, allow: Boolean) { + viewModelScope.launch { + try { + conversationsRepository.allowGuests(token, allow) + _allowGuestsViewState.value = AllowGuestsUIState.Success(allow) + } catch (exception: Exception) { + _allowGuestsViewState.value = AllowGuestsUIState.Error(exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + @SuppressLint("SuspiciousIndentation") + fun setPassword(password: String, token: String) { + viewModelScope.launch { + try { + conversationsRepository.setPassword(password, token) + _passwordViewState.value = PasswordUiState.Success + } catch (exception: Exception) { + _passwordViewState.value = PasswordUiState.Error(exception) + } + } + } + + suspend fun archiveConversation(user: User, token: String) { + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) + val url = ApiUtils.getUrlForArchive(apiVersion, user.baseUrl, token) + conversationsRepository.archiveConversation(user.getCredentials(), url) + } + + suspend fun unarchiveConversation(user: User, token: String) { + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) + val url = ApiUtils.getUrlForArchive(apiVersion, user.baseUrl, token) + conversationsRepository.unarchiveConversation(user.getCredentials(), url) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun markConversationAsImportant(credentials: String, baseUrl: String, roomToken: String) { + viewModelScope.launch { + try { + val response = conversationsRepository.markConversationAsImportant(credentials, baseUrl, roomToken) + _markConversationAsImportantResult.value = + MarkConversationAsImportantViewState.Success(response.ocs?.meta?.statusCode!!) + } catch (exception: Exception) { + _markConversationAsImportantResult.value = + MarkConversationAsImportantViewState.Error(exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun markConversationAsUnimportant(credentials: String, baseUrl: String, roomToken: String) { + viewModelScope.launch { + try { + val response = conversationsRepository.markConversationAsUnImportant(credentials, baseUrl, roomToken) + _markConversationAsUnimportantResult.value = + MarkConversationAsUnimportantViewState.Success(response.ocs?.meta?.statusCode!!) + } catch (exception: Exception) { + _markConversationAsUnimportantResult.value = + MarkConversationAsUnimportantViewState.Error(exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun clearChatHistory(apiVersion: Int, roomToken: String) { + viewModelScope.launch { + try { + conversationsRepository.clearChatHistory(apiVersion, roomToken) + _clearChatHistoryViewState.value = ClearChatHistoryViewState.Success + } catch (exception: Exception) { + _clearChatHistoryViewState.value = ClearChatHistoryViewState.Error(exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun markConversationAsSensitive(credentials: String, baseUrl: String, roomToken: String) { + viewModelScope.launch { + try { + val response = conversationsRepository.markConversationAsSensitive(credentials, baseUrl, roomToken) + _markConversationAsSensitiveResult.value = + MarkConversationAsSensitiveViewState.Success(response.ocs?.meta?.statusCode!!) + } catch (exception: Exception) { + _markConversationAsSensitiveResult.value = + MarkConversationAsSensitiveViewState.Error(exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun markConversationAsInsensitive(credentials: String, baseUrl: String, roomToken: String) { + viewModelScope.launch { + try { + val response = conversationsRepository.markConversationAsInsensitive(credentials, baseUrl, roomToken) + _markConversationAsInsensitiveResult.value = + MarkConversationAsInsensitiveViewState.Success(response.ocs?.meta?.statusCode!!) + } catch (exception: Exception) { + _markConversationAsInsensitiveResult.value = + MarkConversationAsInsensitiveViewState.Error(exception) + } + } + } + + inner class GetRoomObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversationModel: ConversationModel) { + _viewState.value = GetRoomSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching room") + _viewState.value = GetRoomErrorState + } + + override fun onComplete() { + // unused atm + } + } + + companion object { + private val TAG = ConversationInfoViewModel::class.simpleName + private const val NEW_CONVERSATION_PARTICIPANTS_SEPARATOR = ", " + private const val EXTENDED_CONVERSATION = "extended_conversation" + private const val GROUP_CONVERSATION_TYPE = "2" + private const val MAX_ROOM_NAME_LENGTH = 255 + + fun createConversationNameByParticipants( + originalParticipants: List, + allParticipants: List + ): String { + fun List.sortedJoined() = + sortedBy { it?.lowercase() } + .joinToString(NEW_CONVERSATION_PARTICIPANTS_SEPARATOR) + + val addedParticipants = allParticipants - originalParticipants.toSet() + val conversationName = originalParticipants.mapNotNull { it }.sortedJoined() + + NEW_CONVERSATION_PARTICIPANTS_SEPARATOR + + addedParticipants.mapNotNull { it }.sortedJoined() + + return DisplayUtils.ellipsize(conversationName, MAX_ROOM_NAME_LENGTH) + } + } + + sealed class ClearChatHistoryViewState { + data object None : ClearChatHistoryViewState() + data object Success : ClearChatHistoryViewState() + data class Error(val exception: Exception) : ClearChatHistoryViewState() + } + + sealed class MarkConversationAsSensitiveViewState { + data object None : MarkConversationAsSensitiveViewState() + data class Success(val statusCode: Int) : MarkConversationAsSensitiveViewState() + data class Error(val exception: Exception) : MarkConversationAsSensitiveViewState() + } + + sealed class MarkConversationAsInsensitiveViewState { + data object None : MarkConversationAsInsensitiveViewState() + data class Success(val statusCode: Int) : MarkConversationAsInsensitiveViewState() + data class Error(val exception: Exception) : MarkConversationAsInsensitiveViewState() + } + + sealed class SetConversationReadOnlyViewState { + data object None : SetConversationReadOnlyViewState() + data object Success : SetConversationReadOnlyViewState() + data class Error(val exception: Exception) : SetConversationReadOnlyViewState() + } + + sealed class AllowGuestsUIState { + data object None : AllowGuestsUIState() + data class Success(val allow: Boolean) : AllowGuestsUIState() + data class Error(val exception: Exception) : AllowGuestsUIState() + } + + sealed class CreateRoomUIState { + data object None : CreateRoomUIState() + data class Success(val room: RoomOverall) : CreateRoomUIState() + data class Error(val exception: Exception) : CreateRoomUIState() + } + + sealed class PasswordUiState { + data object None : PasswordUiState() + data object Success : PasswordUiState() + data class Error(val exception: Exception) : PasswordUiState() + } + + sealed class MarkConversationAsImportantViewState { + data object None : MarkConversationAsImportantViewState() + data class Success(val statusCode: Int) : MarkConversationAsImportantViewState() + data class Error(val exception: Exception) : MarkConversationAsImportantViewState() + } + + sealed class MarkConversationAsUnimportantViewState { + data object None : MarkConversationAsUnimportantViewState() + data class Success(val statusCode: Int) : MarkConversationAsUnimportantViewState() + data class Error(val exception: Exception) : MarkConversationAsUnimportantViewState() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt new file mode 100644 index 0000000..6af3850 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/ConversationInfoEditActivity.kt @@ -0,0 +1,349 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationinfoedit + +import android.app.Activity +import android.os.Bundle +import android.text.InputFilter +import android.text.TextUtils +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.graphics.drawable.toDrawable +import androidx.core.net.toFile +import androidx.core.view.ViewCompat +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.github.dhaval2404.imagepicker.ImagePicker +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityConversationInfoEditBinding +import com.nextcloud.talk.extensions.loadConversationAvatar +import com.nextcloud.talk.extensions.loadSystemAvatar +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.PickImage +import com.nextcloud.talk.utils.bundle.BundleKeys +import java.io.File +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ConversationInfoEditActivity : BaseActivity() { + + private lateinit var binding: ActivityConversationInfoEditBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + lateinit var conversationInfoEditViewModel: ConversationInfoEditViewModel + + private lateinit var roomToken: String + private lateinit var conversationUser: User + private lateinit var credentials: String + + private var conversation: ConversationModel? = null + + private lateinit var pickImage: PickImage + + private lateinit var spreedCapabilities: SpreedCapability + + private val startImagePickerForResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + handleResult(it) { result -> + pickImage.onImagePickerResult(result.data) { uri -> + uploadAvatar(uri.toFile()) + } + } + } + + private val startSelectRemoteFilesIntentForResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + handleResult(it) { result -> + pickImage.onSelectRemoteFilesResult(startImagePickerForResult, result.data) + } + } + + private val startTakePictureIntentForResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + handleResult(it) { result -> + pickImage.onTakePictureResult(startImagePickerForResult, result.data) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityConversationInfoEditBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + initSystemBars() + + val extras: Bundle? = intent.extras + + conversationUser = currentUserProvider.currentUser.blockingGet() + + roomToken = extras?.getString(BundleKeys.KEY_ROOM_TOKEN)!! + + conversationInfoEditViewModel = + ViewModelProvider(this, viewModelFactory)[ConversationInfoEditViewModel::class.java] + + conversationInfoEditViewModel.getRoom(conversationUser, roomToken) + + viewThemeUtils.material.colorTextInputLayout(binding.conversationNameInputLayout) + viewThemeUtils.material.colorTextInputLayout(binding.conversationDescriptionInputLayout) + + credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)!! + + pickImage = PickImage(this, conversationUser) + + val max = CapabilitiesUtil.conversationDescriptionLength(conversationUser.capabilities?.spreedCapability!!) + binding.conversationDescription.filters = arrayOf( + InputFilter.LengthFilter(max) + ) + binding.conversationDescriptionInputLayout.counterMaxLength = max + + initObservers() + } + + private fun initObservers() { + initViewStateObserver() + conversationInfoEditViewModel.renameRoomUiState.observe(this) { uiState -> + when (uiState) { + is ConversationInfoEditViewModel.RenameRoomUiState.None -> { + } + is ConversationInfoEditViewModel.RenameRoomUiState.Success -> { + if (CapabilitiesUtil.isConversationDescriptionEndpointAvailable(spreedCapabilities)) { + saveConversationDescription() + } else { + finish() + } + } + is ConversationInfoEditViewModel.RenameRoomUiState.Error -> { + Snackbar + .make(binding.root, context.getString(R.string.default_error_msg), Snackbar.LENGTH_LONG) + .show() + Log.e(TAG, "Error while saving conversation name", uiState.exception) + } + } + } + + conversationInfoEditViewModel.setConversationDescriptionUiState.observe(this) { uiState -> + when (uiState) { + is ConversationInfoEditViewModel.SetConversationDescriptionUiState.None -> { + } + is ConversationInfoEditViewModel.SetConversationDescriptionUiState.Success -> { + finish() + } + is ConversationInfoEditViewModel.SetConversationDescriptionUiState.Error -> { + Snackbar + .make(binding.root, context.getString(R.string.default_error_msg), Snackbar.LENGTH_LONG) + .show() + Log.e(TAG, "Error while saving conversation description", uiState.exception) + } + } + } + } + + private fun initViewStateObserver() { + conversationInfoEditViewModel.viewState.observe(this) { state -> + when (state) { + is ConversationInfoEditViewModel.GetRoomSuccessState -> { + conversation = state.conversationModel + + spreedCapabilities = conversationUser.capabilities!!.spreedCapability!! + + binding.conversationName.setText(conversation!!.displayName) + + if (conversation!!.description.isNotEmpty()) { + binding.conversationDescription.setText(conversation!!.description) + } + + if (!CapabilitiesUtil.isConversationDescriptionEndpointAvailable(spreedCapabilities)) { + binding.conversationDescription.isEnabled = false + } + + if (conversation?.objectType == ConversationEnums.ObjectType.EVENT) { + binding.conversationName.isEnabled = false + binding.conversationDescription.isEnabled = false + } + + loadConversationAvatar() + } + + is ConversationInfoEditViewModel.GetRoomErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + is ConversationInfoEditViewModel.UploadAvatarSuccessState -> { + conversation = state.conversationModel + loadConversationAvatar() + } + + is ConversationInfoEditViewModel.UploadAvatarErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + is ConversationInfoEditViewModel.DeleteAvatarSuccessState -> { + conversation = state.conversationModel + loadConversationAvatar() + } + + is ConversationInfoEditViewModel.DeleteAvatarErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + } + + private fun setupAvatarOptions() { + binding.avatarUpload.setOnClickListener { + pickImage.selectLocal(startImagePickerForResult = startImagePickerForResult) + } + + binding.avatarChoose.setOnClickListener { + pickImage.selectRemote(startSelectRemoteFilesIntentForResult = startSelectRemoteFilesIntentForResult) + } + + binding.avatarCamera.setOnClickListener { + pickImage.takePicture(startTakePictureIntentForResult = startTakePictureIntentForResult) + } + + if (conversation?.hasCustomAvatar == true) { + binding.avatarDelete.visibility = View.VISIBLE + binding.avatarDelete.setOnClickListener { deleteAvatar() } + } else { + binding.avatarDelete.visibility = View.GONE + } + + binding.avatarImage.let { ViewCompat.setTransitionName(it, "userAvatar.transitionTag") } + + binding.let { + viewThemeUtils.material.themeFAB(it.avatarUpload) + viewThemeUtils.material.themeFAB(it.avatarChoose) + viewThemeUtils.material.themeFAB(it.avatarCamera) + viewThemeUtils.material.themeFAB(it.avatarDelete) + } + } + + private fun setupActionBar() { + setSupportActionBar(binding.conversationInfoEditToolbar) + binding.conversationInfoEditToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(android.R.color.transparent, null).toDrawable()) + supportActionBar?.title = resources!!.getString(R.string.nc_conversation_menu_conversation_info) + + viewThemeUtils.material.themeToolbar(binding.conversationInfoEditToolbar) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.menu_conversation_info_edit, menu) + return true + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + super.onPrepareOptionsMenu(menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.save) { + if (conversation?.objectType != ConversationEnums.ObjectType.EVENT) { + saveConversationNameAndDescription() + } + } + return true + } + + private fun saveConversationNameAndDescription() { + val newRoomName = binding.conversationName.text.toString() + conversationInfoEditViewModel.renameRoom( + conversation!!.token, + newRoomName + ) + } + + private fun saveConversationDescription() { + val conversationDescription = binding.conversationDescription.text.toString() + conversationInfoEditViewModel.setConversationDescription(conversation!!.token, conversationDescription) + } + + private fun handleResult(result: ActivityResult, onResult: (result: ActivityResult) -> Unit) { + when (result.resultCode) { + Activity.RESULT_OK -> onResult(result) + + ImagePicker.RESULT_ERROR -> { + Snackbar.make(binding.root, ImagePicker.getError(result.data), Snackbar.LENGTH_SHORT).show() + } + + else -> { + Log.i(TAG, "Task Cancelled") + } + } + } + + private fun uploadAvatar(file: File) { + conversationInfoEditViewModel.uploadConversationAvatar(conversationUser, file, roomToken) + } + + private fun deleteAvatar() { + conversationInfoEditViewModel.deleteConversationAvatar(conversationUser, roomToken) + } + + private fun loadConversationAvatar() { + setupAvatarOptions() + + when (conversation!!.type) { + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty( + conversation!!.name + ) + ) { + conversation!!.name.let { binding.avatarImage.loadUserAvatar(conversationUser, it, true, false) } + } + + ConversationEnums.ConversationType.ROOM_GROUP_CALL, ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> { + binding.avatarImage.loadConversationAvatar(conversationUser, conversation!!, false, viewThemeUtils) + } + + ConversationEnums.ConversationType.ROOM_SYSTEM -> { + binding.avatarImage.loadSystemAvatar() + } + + else -> { + // unused atm + } + } + } + + companion object { + private val TAG = ConversationInfoEditActivity::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepository.kt new file mode 100644 index 0000000..7b3571b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepository.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationinfoedit.data + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.generic.GenericOverall +import io.reactivex.Observable +import java.io.File + +interface ConversationInfoEditRepository { + + fun uploadConversationAvatar(user: User, file: File, roomToken: String): Observable + + fun deleteConversationAvatar(user: User, roomToken: String): Observable + + suspend fun renameConversation(roomToken: String, roomNameNew: String): GenericOverall + + suspend fun setConversationDescription(roomToken: String, conversationDescription: String?): GenericOverall +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt new file mode 100644 index 0000000..1f63af6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/data/ConversationInfoEditRepositoryImpl.kt @@ -0,0 +1,88 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationinfoedit.data + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.Mimetype +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File + +class ConversationInfoEditRepositoryImpl( + private val ncApi: NcApi, + private val ncApiCoroutines: NcApiCoroutines, + currentUserProvider: CurrentUserProviderNew +) : ConversationInfoEditRepository { + + val currentUser: User = currentUserProvider.currentUser.blockingGet() + val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!! + + val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) + + override fun uploadConversationAvatar(user: User, file: File, roomToken: String): Observable { + val builder = MultipartBody.Builder() + builder.setType(MultipartBody.FORM) + builder.addFormDataPart( + "file", + file.name, + file.asRequestBody(Mimetype.IMAGE_PREFIX_GENERIC.toMediaTypeOrNull()) + ) + val filePart: MultipartBody.Part = MultipartBody.Part.createFormData( + "file", + file.name, + file.asRequestBody(Mimetype.IMAGE_JPG.toMediaTypeOrNull()) + ) + + return ncApi.uploadConversationAvatar( + credentials, + ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken), + filePart + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) } + } + + override fun deleteConversationAvatar(user: User, roomToken: String): Observable = + ncApi.deleteConversationAvatar( + credentials, + ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken) + ).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) } + + override suspend fun renameConversation(roomToken: String, newRoomName: String): GenericOverall { + val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) + + return ncApiCoroutines.renameRoom( + credentials, + ApiUtils.getUrlForRoom( + apiVersion, + currentUser.baseUrl!!, + roomToken + ), + newRoomName + ) + } + + override suspend fun setConversationDescription( + roomToken: String, + conversationDescription: String? + ): GenericOverall = + ncApiCoroutines.setConversationDescription( + credentials, + ApiUtils.getUrlForConversationDescription( + apiVersion, + currentUser.baseUrl!!, + roomToken + ), + conversationDescription + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt new file mode 100644 index 0000000..f62bba5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationinfoedit/viewmodel/ConversationInfoEditViewModel.kt @@ -0,0 +1,178 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationinfoedit.viewmodel + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.launch +import java.io.File +import javax.inject.Inject + +class ConversationInfoEditViewModel @Inject constructor( + private val repository: ChatNetworkDataSource, + private val conversationInfoEditRepository: ConversationInfoEditRepository +) : ViewModel() { + + sealed interface ViewState + + object GetRoomStartState : ViewState + object GetRoomErrorState : ViewState + open class GetRoomSuccessState(val conversationModel: ConversationModel) : ViewState + + object UploadAvatarErrorState : ViewState + open class UploadAvatarSuccessState(val conversationModel: ConversationModel) : ViewState + + object DeleteAvatarErrorState : ViewState + open class DeleteAvatarSuccessState(val conversationModel: ConversationModel) : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(GetRoomStartState) + val viewState: LiveData + get() = _viewState + + private val _renameRoomUiState = MutableLiveData(RenameRoomUiState.None) + val renameRoomUiState: LiveData + get() = _renameRoomUiState + + private val _setConversationDescriptionUiState = + MutableLiveData(SetConversationDescriptionUiState.None) + val setConversationDescriptionUiState: LiveData + get() = _setConversationDescriptionUiState + + fun getRoom(user: User, token: String) { + _viewState.value = GetRoomStartState + repository.getRoom(user, token) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(GetRoomObserver()) + } + + fun uploadConversationAvatar(user: User, file: File, roomToken: String) { + conversationInfoEditRepository.uploadConversationAvatar(user, file, roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(UploadConversationAvatarObserver()) + } + + fun deleteConversationAvatar(user: User, roomToken: String) { + conversationInfoEditRepository.deleteConversationAvatar(user, roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(DeleteConversationAvatarObserver()) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun renameRoom(roomToken: String, newRoomName: String) { + viewModelScope.launch { + try { + conversationInfoEditRepository.renameConversation(roomToken, newRoomName) + _renameRoomUiState.value = RenameRoomUiState.Success + } catch (exception: Exception) { + _renameRoomUiState.value = RenameRoomUiState.Error(exception) + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun setConversationDescription(roomToken: String, conversationDescription: String?) { + viewModelScope.launch { + try { + conversationInfoEditRepository.setConversationDescription( + roomToken, + conversationDescription + ) + + _setConversationDescriptionUiState.value = SetConversationDescriptionUiState.Success + } catch (exception: Exception) { + _setConversationDescriptionUiState.value = SetConversationDescriptionUiState.Error(exception) + } + } + } + + inner class GetRoomObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversationModel: ConversationModel) { + _viewState.value = GetRoomSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching room") + _viewState.value = GetRoomErrorState + } + + override fun onComplete() { + // unused atm + } + } + + inner class UploadConversationAvatarObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversationModel: ConversationModel) { + _viewState.value = UploadAvatarSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when uploading avatar") + _viewState.value = UploadAvatarErrorState + } + + override fun onComplete() { + // unused atm + } + } + + inner class DeleteConversationAvatarObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversationModel: ConversationModel) { + _viewState.value = DeleteAvatarSuccessState(conversationModel) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when deleting avatar") + _viewState.value = DeleteAvatarErrorState + } + + override fun onComplete() { + // unused atm + } + } + + companion object { + private val TAG = ConversationInfoEditViewModel::class.simpleName + } + + sealed class RenameRoomUiState { + data object None : RenameRoomUiState() + data object Success : RenameRoomUiState() + data class Error(val exception: Exception) : RenameRoomUiState() + } + + sealed class SetConversationDescriptionUiState { + data object None : SetConversationDescriptionUiState() + data object Success : SetConversationDescriptionUiState() + data class Error(val exception: Exception) : SetConversationDescriptionUiState() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt new file mode 100644 index 0000000..06324a6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -0,0 +1,2279 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022-2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2022-2023 Andy Scherzinger + * SPDX-FileCopyrightText: 2023 Tobias Kaminsky + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationlist + +import android.Manifest +import android.animation.AnimatorInflater +import android.annotation.SuppressLint +import android.app.SearchManager +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.provider.Settings +import android.text.InputType +import android.text.TextUtils +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.animation.AnimationUtils +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.annotation.OptIn +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.SearchView +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import androidx.core.graphics.drawable.toDrawable +import androidx.core.net.toUri +import androidx.core.os.bundleOf +import androidx.core.view.MenuItemCompat +import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import coil.imageLoader +import coil.request.ImageRequest +import coil.target.Target +import coil.transform.CircleCropTransformation +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils +import com.google.android.material.badge.ExperimentalBadgeUtils +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.account.ServerSelectionActivity +import com.nextcloud.talk.account.WebViewLoginActivity +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.activities.CallActivity +import com.nextcloud.talk.activities.MainActivity +import com.nextcloud.talk.adapters.items.ContactItem +import com.nextcloud.talk.adapters.items.ConversationItem +import com.nextcloud.talk.adapters.items.GenericTextHeaderItem +import com.nextcloud.talk.adapters.items.LoadMoreResultsItem +import com.nextcloud.talk.adapters.items.MessageResultItem +import com.nextcloud.talk.adapters.items.MessagesTextHeaderItem +import com.nextcloud.talk.adapters.items.SpacerItem +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.contacts.ContactsActivity +import com.nextcloud.talk.contacts.ContactsUiState +import com.nextcloud.talk.contacts.ContactsViewModel +import com.nextcloud.talk.contacts.RoomUiState +import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel +import com.nextcloud.talk.data.network.NetworkMonitor +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityConversationsBinding +import com.nextcloud.talk.events.ConversationsListFetchDataEvent +import com.nextcloud.talk.events.EventStatus +import com.nextcloud.talk.invitation.InvitationsActivity +import com.nextcloud.talk.jobs.AccountRemovalWorker +import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.run +import com.nextcloud.talk.jobs.DeleteConversationWorker +import com.nextcloud.talk.jobs.UploadAndShareFilesWorker +import com.nextcloud.talk.messagesearch.MessageSearchHelper +import com.nextcloud.talk.messagesearch.MessageSearchHelper.MessageSearchResults +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.conversations.RoomsOverall +import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository +import com.nextcloud.talk.settings.SettingsActivity +import com.nextcloud.talk.threadsoverview.ThreadsOverviewActivity +import com.nextcloud.talk.ui.BackgroundVoiceMessageCard +import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment +import com.nextcloud.talk.ui.dialog.ChooseAccountShareToDialogFragment +import com.nextcloud.talk.ui.dialog.ContextChatCompose +import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog +import com.nextcloud.talk.ui.dialog.FilterConversationFragment +import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.ARCHIVE +import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.MENTION +import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.UNREAD +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.BrandingUtils +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.CapabilitiesUtil.isServerEOL +import com.nextcloud.talk.utils.CapabilitiesUtil.isUnifiedSearchAvailable +import com.nextcloud.talk.utils.ClosedInterfaceImpl +import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.FileUtils +import com.nextcloud.talk.utils.Mimetype +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.ParticipantPermissions +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.UserIdUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ADDITIONAL_ACCOUNT +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_FLAG +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_MSG_TEXT +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SCROLL_TO_NOTIFICATION_CATEGORY +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARED_TEXT +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil +import com.nextcloud.talk.utils.power.PowerManagerUtils +import com.nextcloud.talk.utils.rx.SearchViewObservable.Companion.observeSearchView +import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.apache.commons.lang3.builder.CompareToBuilder +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import retrofit2.HttpException +import java.io.File +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@SuppressLint("StringFormatInvalid") +@AutoInjector(NextcloudTalkApplication::class) +class ConversationsListActivity : + BaseActivity(), + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener { + + private lateinit var binding: ActivityConversationsBinding + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var unifiedSearchRepository: UnifiedSearchRepository + + @Inject + lateinit var platformPermissionUtil: PlatformPermissionUtil + + @Inject + lateinit var arbitraryStorageManager: ArbitraryStorageManager + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var networkMonitor: NetworkMonitor + + @Inject + lateinit var chatViewModel: ChatViewModel + + @Inject + lateinit var contactsViewModel: ContactsViewModel + + lateinit var conversationsListViewModel: ConversationsListViewModel + + override val appBarLayoutType: AppBarLayoutType + get() = AppBarLayoutType.SEARCH_BAR + + private var currentUser: User? = null + private var roomsQueryDisposable: Disposable? = null + private var openConversationsQueryDisposable: Disposable? = null + private var adapter: FlexibleAdapter>? = null + private var conversationItems: MutableList> = ArrayList() + private var conversationItemsWithHeader: MutableList> = ArrayList() + private var searchableConversationItems: MutableList> = ArrayList() + private var filterableConversationItems: MutableList> = ArrayList() + private var nearFutureEventConversationItems: MutableList> = ArrayList() + private var searchItem: MenuItem? = null + private var chooseAccountItem: MenuItem? = null + private var searchView: SearchView? = null + private var searchQuery: String? = null + private var credentials: String? = null + private var adapterWasNull = true + private var isRefreshing = false + private var showShareToScreen = false + private var filesToShare: ArrayList? = null + private var selectedConversation: ConversationModel? = null + private var textToPaste: String? = "" + private var selectedMessageId: String? = null + private var forwardMessage: Boolean = false + private var nextUnreadConversationScrollPosition = 0 + private var layoutManager: SmoothScrollLinearLayoutManager? = null + private val callHeaderItems = HashMap() + private var conversationsListBottomDialog: ConversationsListBottomDialog? = null + private var searchHelper: MessageSearchHelper? = null + private var searchViewDisposable: Disposable? = null + private var filterState = + mutableMapOf( + MENTION to false, + UNREAD to false, + ARCHIVE to false, + FilterConversationFragment.DEFAULT to true + ) + val searchBehaviorSubject = BehaviorSubject.createDefault(false) + private lateinit var accountIconBadge: BadgeDrawable + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (forwardMessage) { + finish() + } else { + finishAffinity() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + currentUser = currentUserProvider.currentUser.blockingGet() + + conversationsListViewModel = ViewModelProvider(this, viewModelFactory)[ConversationsListViewModel::class.java] + + binding = ActivityConversationsBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + initSystemBars() + viewThemeUtils.material.themeSearchCardView(binding.searchToolbar) + viewThemeUtils.material.colorMaterialButtonContent(binding.menuButton, ColorRole.ON_SURFACE_VARIANT) + viewThemeUtils.platform.colorTextView(binding.searchText, ColorRole.ON_SURFACE_VARIANT) + + forwardMessage = intent.getBooleanExtra(KEY_FORWARD_MSG_FLAG, false) + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + + initObservers() + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + + // handle notification permission on API level >= 33 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + !platformPermissionUtil.isPostNotificationsPermissionGranted() && + ClosedInterfaceImpl().isGooglePlayServicesAvailable + ) { + requestPermissions( + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + REQUEST_POST_NOTIFICATIONS_PERMISSION + ) + } + } + + override fun onResume() { + super.onResume() + if (adapter == null) { + adapter = FlexibleAdapter(conversationItems, this, true) + addEmptyItemForEdgeToEdgeIfNecessary() + } else { + binding.loadingContent.visibility = View.GONE + } + adapter?.addListener(this) + prepareViews() + + showNotificationWarning() + + showShareToScreen = hasActivityActionSendIntent() + + if (!eventBus.isRegistered(this)) { + eventBus.register(this) + } + + if (currentUser != null) { + if (isServerEOL(currentUser!!.serverVersion?.major)) { + showServerEOLDialog() + return + } + currentUser?.capabilities?.spreedCapability?.let { spreedCapabilities -> + if (isUnifiedSearchAvailable(spreedCapabilities)) { + searchHelper = MessageSearchHelper(unifiedSearchRepository) + } + } + credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) + + loadUserAvatar(binding.switchAccountButton) + viewThemeUtils.material.colorMaterialTextButton(binding.switchAccountButton) + viewThemeUtils.material.themeCardView(binding.conversationListHintInclude.hintLayoutCardview) + viewThemeUtils.material.themeCardView( + binding.conversationListNotificationWarning.notificationWarningCardview + ) + viewThemeUtils.material.colorMaterialButtonText(binding.conversationListNotificationWarning.notNowButton) + viewThemeUtils.material.colorMaterialButtonText( + binding.conversationListNotificationWarning.showSettingsButton + ) + searchBehaviorSubject.onNext(false) + fetchRooms() + fetchPendingInvitations() + } else { + Log.e(TAG, "currentUser was null") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + showSearchOrToolbar() + conversationsListViewModel.checkIfThreadsExist() + } + + override fun onPause() { + super.onPause() + val firstVisible = layoutManager?.findFirstVisibleItemPosition() ?: 0 + val firstItem = adapter?.getItem(firstVisible) + val firstTop = (firstItem as? ConversationItem)?.mHolder?.itemView?.top + val firstOffset = firstTop?.minus(CONVERSATION_ITEM_HEIGHT) ?: 0 + + appPreferences.setConversationListPositionAndOffset(firstVisible, firstOffset) + } + + // if edge to edge is used, add an empty item at the bottom of the list + @Suppress("MagicNumber") + private fun addEmptyItemForEdgeToEdgeIfNecessary() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + adapter?.addScrollableFooter(SpacerItem(100)) + } + } + + @Suppress("LongMethod") + private fun initObservers() { + this.lifecycleScope.launch { + networkMonitor.isOnline.onEach { isOnline -> + showNetworkErrorDialog(!isOnline) + handleUI(isOnline) + }.collect() + } + + conversationsListViewModel.getFederationInvitationsViewState.observe(this) { state -> + when (state) { + is ConversationsListViewModel.GetFederationInvitationsStartState -> { + binding.conversationListHintInclude.conversationListHintLayout.visibility = View.GONE + } + + is ConversationsListViewModel.GetFederationInvitationsSuccessState -> { + binding.conversationListHintInclude.conversationListHintLayout.visibility = + if (state.showInvitationsHint) View.VISIBLE else View.GONE + } + + is ConversationsListViewModel.GetFederationInvitationsErrorState -> { + // do nothing + } + + else -> {} + } + } + + conversationsListViewModel.showBadgeViewState.observe(this) { state -> + when (state) { + is ConversationsListViewModel.ShowBadgeStartState -> { + showAccountIconBadge(false) + } + + is ConversationsListViewModel.ShowBadgeSuccessState -> { + showAccountIconBadge(state.showBadge) + } + + is ConversationsListViewModel.ShowBadgeErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + + conversationsListViewModel.getRoomsViewState.observe(this) { state -> + when (state) { + is ConversationsListViewModel.GetRoomsSuccessState -> { + if (adapterWasNull) { + adapterWasNull = false + binding.loadingContent.visibility = View.GONE + } + initOverallLayout(state.listIsNotEmpty) + binding.swipeRefreshLayoutView.isRefreshing = false + } + + is ConversationsListViewModel.GetRoomsErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_SHORT).show() + } + + else -> {} + } + } + + lifecycleScope.launch { + conversationsListViewModel.threadsExistState.collect { state -> + when (state) { + is ConversationsListViewModel.ThreadsExistUiState.Success -> { + binding.threadsButton.visibility = if (state.threadsExistence == true) { + View.VISIBLE + } else { + View.GONE + } + } + else -> { + binding.threadsButton.visibility = View.GONE + } + } + } + } + + lifecycleScope.launch { + conversationsListViewModel.getRoomsFlow + .onEach { list -> + setConversationList(list) + val noteToSelf = list + .firstOrNull { ConversationUtils.isNoteToSelfConversation(it) } + val isNoteToSelfAvailable = noteToSelf != null + handleNoteToSelfShortcut(isNoteToSelfAvailable, noteToSelf?.token ?: "") + + val pair = appPreferences.conversationListPositionAndOffset + layoutManager?.scrollToPositionWithOffset(pair.first, pair.second) + }.collect() + } + + lifecycleScope.launch { + contactsViewModel.roomViewState.onEach { state -> + when (state) { + is RoomUiState.Success -> { + val conversation = state.conversation + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, conversation?.token) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(chatIntent) + } + + else -> {} + } + }.collect() + } + + lifecycleScope.launch { + contactsViewModel.contactsViewState.onEach { state -> + when (state) { + is ContactsUiState.Success -> { + if (state.contacts.isNullOrEmpty()) return@onEach + + val userItems: MutableList> = ArrayList() + val actorTypeConverter = EnumActorTypeConverter() + var genericTextHeaderItem: GenericTextHeaderItem + for (autocompleteUser in state.contacts) { + val headerTitle = resources!!.getString(R.string.nc_user) + if (!callHeaderItems.containsKey(headerTitle)) { + genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils) + callHeaderItems[headerTitle] = genericTextHeaderItem + } + + val participant = Participant() + participant.actorId = autocompleteUser.id + participant.actorType = actorTypeConverter.getFromString(autocompleteUser.source) + participant.displayName = autocompleteUser.label + + val contactItem = ContactItem( + participant, + currentUser!!, + callHeaderItems[headerTitle], + viewThemeUtils + ) + + userItems.add(contactItem) + } + + val list = searchableConversationItems.filter { + it !is ContactItem + }.toMutableList() + + list.addAll(userItems) + + searchableConversationItems = list + } + + else -> {} + } + }.collect() + } + + lifecycleScope.launch { + chatViewModel.backgroundPlayUIFlow.onEach { msg -> + binding.composeViewForBackgroundPlay.apply { + // Dispose of the Composition when the view's LifecycleOwner is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + msg?.let { + val duration = chatViewModel.mediaPlayerDuration + val position = chatViewModel.mediaPlayerPosition + val offset = position.toFloat() / duration + val imageURI = ApiUtils.getUrlForAvatar( + currentUser?.baseUrl, + msg.actorId, + true + ) + val conversationImageURI = ApiUtils.getUrlForConversationAvatar( + ApiUtils.API_V1, + currentUser?.baseUrl, + msg.token + ) + + if (duration > 0) { + BackgroundVoiceMessageCard( + msg.actorDisplayName!!, + duration - position, + offset, + imageURI, + conversationImageURI, + viewThemeUtils, + context + ) + .GetView({ isPaused -> + if (isPaused) { + chatViewModel.pauseMediaPlayer(false) + } else { + val filename = msg.selectedIndividualHashMap!!["name"] + val file = File(context.cacheDir, filename!!) + chatViewModel.startMediaPlayer(file.canonicalPath) + } + }) { + chatViewModel.stopMediaPlayer() + } + } + } + } + } + }.collect() + } + } + + private fun handleNoteToSelfShortcut(noteToSelfAvailable: Boolean, noteToSelfToken: String) { + if (noteToSelfAvailable) { + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, noteToSelfToken) + bundle.putBoolean(BundleKeys.KEY_FOCUS_INPUT, true) + val intent = Intent(context, ChatActivity::class.java) + intent.putExtras(bundle) + intent.action = Intent.ACTION_VIEW + val openNotesString = resources.getString(R.string.open_notes) + + val shortcut = ShortcutInfoCompat.Builder(context, NOTE_TO_SELF_SHORTCUT_ID) + .setShortLabel(openNotesString) + .setLongLabel(openNotesString) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_pencil_grey600_24dp)) + .setIntent(intent) + .build() + + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } else { + ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(NOTE_TO_SELF_SHORTCUT_ID)) + } + } + + private fun setConversationList(list: List) { + // Update Conversations + conversationItems.clear() + conversationItemsWithHeader.clear() + nearFutureEventConversationItems.clear() + + for (conversation in list) { + if (!isFutureEvent(conversation) && !conversation.hasArchived) { + addToNearFutureEventConversationItems(conversation) + } + addToConversationItems(conversation) + } + + getFilterStates() + val noFiltersActive = !( + filterState[MENTION] == true || + filterState[UNREAD] == true || + filterState[ARCHIVE] == true + ) + + sortConversations(conversationItems) + sortConversations(conversationItemsWithHeader) + sortConversations(nearFutureEventConversationItems) + + if (noFiltersActive && searchBehaviorSubject.value == false) { + adapter?.updateDataSet(nearFutureEventConversationItems, false) + } else { + applyFilter() + } + + Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong()) + + // Fetch Open Conversations + val apiVersion = ApiUtils.getConversationApiVersion( + currentUser!!, + intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1) + ) + fetchOpenConversations(apiVersion) + } + + fun applyFilter() { + if (!hasFilterEnabled()) { + filterableConversationItems = conversationItems + } + filterConversation() + adapter?.updateDataSet(filterableConversationItems, false) + } + + private fun hasFilterEnabled(): Boolean { + for ((k, v) in filterState) { + if (k != FilterConversationFragment.DEFAULT && v) return true + } + + return false + } + + private fun isFutureEvent(conversation: ConversationModel): Boolean { + if (!conversation.objectId.contains("#")) { + return false + } + val eventTimeStart = conversation.objectId.split("#")[0].toLong() + val currentTimeStampInSeconds = System.currentTimeMillis() / LONG_1000 + val sixteenHoursAfterTimeStamp = (eventTimeStart - currentTimeStampInSeconds) > SIXTEEN_HOURS_IN_SECONDS + return conversation.objectType == ConversationEnums.ObjectType.EVENT && sixteenHoursAfterTimeStamp + } + + fun showOnlyNearFutureEvents() { + sortConversations(nearFutureEventConversationItems) + adapter?.updateDataSet(nearFutureEventConversationItems, false) + adapter?.smoothScrollToPosition(0) + } + + private fun addToNearFutureEventConversationItems(conversation: ConversationModel) { + val conversationItem = ConversationItem(conversation, currentUser!!, this, null, viewThemeUtils) + nearFutureEventConversationItems.add(conversationItem) + } + + fun getFilterStates() { + val accountId = UserIdUtils.getIdForUser(currentUser) + filterState[UNREAD] = ( + arbitraryStorageManager.getStorageSetting( + accountId, + UNREAD, + "" + ).blockingGet()?.value ?: "" + ) == "true" + + filterState[MENTION] = ( + arbitraryStorageManager.getStorageSetting( + accountId, + MENTION, + "" + ).blockingGet()?.value ?: "" + ) == "true" + + filterState[ARCHIVE] = ( + arbitraryStorageManager.getStorageSetting( + accountId, + ARCHIVE, + "" + ).blockingGet()?.value ?: "" + ) == "true" + } + + fun filterConversation() { + getFilterStates() + val newItems: MutableList> = ArrayList() + val items = conversationItems + for (i in items) { + val conversation = (i as ConversationItem).model + if (filter(conversation)) { + newItems.add(i) + } + } + + val archiveFilterOn = filterState[ARCHIVE] == true + if (archiveFilterOn && newItems.isEmpty()) { + binding.noArchivedConversationLayout.visibility = View.VISIBLE + } else { + binding.noArchivedConversationLayout.visibility = View.GONE + } + + adapter?.updateDataSet(newItems, true) + setFilterableItems(newItems) + if (archiveFilterOn) { + // Never a notification from archived conversations + binding.newMentionPopupBubble.visibility = View.GONE + } + + layoutManager?.scrollToPositionWithOffset(0, 0) + updateFilterConversationButtonColor() + } + + private fun filter(conversation: ConversationModel): Boolean { + var result = true + for ((k, v) in filterState) { + if (v) { + when (k) { + MENTION -> result = (result && conversation.unreadMention) || + ( + result && + ( + conversation.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL || + conversation.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE + ) && + (conversation.unreadMessages > 0) + ) + + UNREAD -> result = result && (conversation.unreadMessages > 0) + + FilterConversationFragment.DEFAULT -> { + result = if (filterState[ARCHIVE] == true) { + result && conversation.hasArchived + } else { + result && !conversation.hasArchived + } + } + } + } + } + + Log.d(TAG, "Conversation: ${conversation.name} Result: $result") + return result + } + + private fun setupActionBar() { + setSupportActionBar(binding.conversationListToolbar) + binding.conversationListToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(R.color.transparent, null).toDrawable()) + supportActionBar?.title = resources!!.getString(R.string.nc_app_product_name) + viewThemeUtils.material.themeToolbar(binding.conversationListToolbar) + } + + private fun loadUserAvatar(target: Target) { + if (currentUser != null) { + val url = ApiUtils.getUrlForAvatar( + currentUser!!.baseUrl!!, + currentUser!!.userId, + true + ) + + val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) + + context.imageLoader.enqueue( + ImageRequest.Builder(context) + .data(url) + .addHeader("Authorization", credentials!!) + .placeholder(R.drawable.ic_user) + .transformations(CircleCropTransformation()) + .crossfade(true) + .target(target) + .build() + ) + } else { + Log.e(TAG, "currentUser was null in loadUserAvatar") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + } + + private fun loadUserAvatar(button: MaterialButton) { + val target = object : Target { + override fun onStart(placeholder: Drawable?) { + button.icon = placeholder + } + + override fun onSuccess(result: Drawable) { + button.icon = result + } + } + + loadUserAvatar(target) + } + + private fun loadUserAvatar(menuItem: MenuItem) { + val target = object : Target { + override fun onStart(placeholder: Drawable?) { + menuItem.icon = placeholder + } + + override fun onSuccess(result: Drawable) { + menuItem.icon = result + } + } + loadUserAvatar(target) + } + + private fun initSearchView() { + val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager? + if (searchItem != null) { + searchView = MenuItemCompat.getActionView(searchItem) as SearchView + viewThemeUtils.talk.themeSearchView(searchView!!) + searchView!!.maxWidth = Int.MAX_VALUE + searchView!!.inputType = InputType.TYPE_TEXT_VARIATION_FILTER + var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN + if (appPreferences.isKeyboardIncognito) { + imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } + searchView!!.imeOptions = imeOptions + searchView!!.queryHint = getString(R.string.appbar_search_in, getString(R.string.nc_app_product_name)) + if (searchManager != null) { + searchView!!.setSearchableInfo(searchManager.getSearchableInfo(componentName)) + } + initSearchDisposable() + } + } + + private fun initSearchDisposable() { + if (searchViewDisposable == null || searchViewDisposable?.isDisposed == true) { + searchViewDisposable = observeSearchView(searchView!!) + .debounce { query: String? -> + if (TextUtils.isEmpty(query)) { + return@debounce Observable.empty() + } else { + return@debounce Observable.timer( + SEARCH_DEBOUNCE_INTERVAL_MS.toLong(), + TimeUnit.MILLISECONDS + ) + } + } + .distinctUntilChanged() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { newText: String? -> onQueryTextChange(newText) } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + + menuInflater.inflate(R.menu.menu_conversation_plus_filter, menu) + searchItem = menu.findItem(R.id.action_search) + chooseAccountItem = menu.findItem(R.id.action_choose_account) + loadUserAvatar(chooseAccountItem!!) + + chooseAccountItem?.setOnMenuItemClickListener { + if (resources != null && resources!!.getBoolean(R.bool.multiaccount_support)) { + val newFragment: DialogFragment = ChooseAccountShareToDialogFragment.newInstance() + newFragment.show( + supportFragmentManager, + ChooseAccountShareToDialogFragment.TAG + ) + } + true + } + initSearchView() + return true + } + + @OptIn(ExperimentalBadgeUtils::class) + fun showAccountIconBadge(showBadge: Boolean) { + if (!::accountIconBadge.isInitialized) { + accountIconBadge = BadgeDrawable.create(binding.switchAccountButton.context) + accountIconBadge.verticalOffset = BADGE_OFFSET + accountIconBadge.horizontalOffset = BADGE_OFFSET + accountIconBadge.backgroundColor = resources.getColor(R.color.badge_color, null) + } + + if (showBadge) { + BadgeUtils.attachBadgeDrawable(accountIconBadge, binding.switchAccountButton) + } else { + BadgeUtils.detachBadgeDrawable(accountIconBadge, binding.switchAccountButton) + } + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + super.onPrepareOptionsMenu(menu) + + searchView = MenuItemCompat.getActionView(searchItem) as SearchView + + val moreAccountsAvailable = userManager.users.blockingGet().size > 1 + menu.findItem(R.id.action_choose_account).isVisible = showShareToScreen && moreAccountsAvailable + + if (showShareToScreen) { + hideSearchBar() + supportActionBar?.setTitle(R.string.send_to_three_dots) + } else if (forwardMessage) { + hideSearchBar() + supportActionBar?.setTitle(R.string.nc_forward_to_three_dots) + } else { + searchItem!!.isVisible = conversationItems.size > 0 + if (adapter?.hasFilter() == true) { + showSearchView(searchView, searchItem) + searchView!!.setQuery(adapter?.getFilter(String::class.java), false) + } + binding.searchText.setOnClickListener { + showSearchView(searchView, searchItem) + viewThemeUtils.platform.themeStatusBar(this) + } + searchView!!.findViewById(R.id.search_close_btn).setOnClickListener { + if (TextUtils.isEmpty(searchView!!.query.toString())) { + searchView!!.onActionViewCollapsed() + viewThemeUtils.platform.resetStatusBar(this) + } else { + resetSearchResults() + searchView!!.setQuery("", false) + } + } + + searchView!!.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(p0: String?): Boolean { + initSearchDisposable() + return true + } + + override fun onQueryTextChange(p0: String?): Boolean { + this@ConversationsListActivity.onQueryTextChange(p0) + return true + } + }) + + searchItem!!.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + initSearchDisposable() + adapter?.setHeadersShown(true) + if (!hasFilterEnabled()) filterableConversationItems = searchableConversationItems + adapter!!.updateDataSet(filterableConversationItems, false) + adapter!!.showAllHeaders() + binding.swipeRefreshLayoutView.isEnabled = false + searchBehaviorSubject.onNext(true) + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + adapter?.setHeadersShown(false) + searchBehaviorSubject.onNext(false) + if (!hasFilterEnabled()) filterableConversationItems = conversationItemsWithHeader + if (!hasFilterEnabled()) { + adapter?.updateDataSet(nearFutureEventConversationItems, false) + } else { + filterableConversationItems = conversationItems + } + adapter?.hideAllHeaders() + if (searchHelper != null) { + // cancel any pending searches + searchHelper!!.cancelSearch() + } + binding.swipeRefreshLayoutView.isRefreshing = false + binding.swipeRefreshLayoutView.isEnabled = true + searchView!!.onActionViewCollapsed() + + binding.conversationListAppbar.stateListAnimator = AnimatorInflater.loadStateListAnimator( + binding.conversationListAppbar.context, + R.animator.appbar_elevation_off + ) + binding.conversationListToolbar.visibility = View.GONE + binding.searchToolbar.visibility = View.VISIBLE + if (resources != null) { + viewThemeUtils.platform.resetStatusBar(this@ConversationsListActivity) + } + + val layoutManager = binding.recyclerView.layoutManager as SmoothScrollLinearLayoutManager? + layoutManager?.scrollToPositionWithOffset(0, 0) + return true + } + }) + } + return true + } + + private fun showSearchOrToolbar() { + if (TextUtils.isEmpty(searchQuery)) { + if (appBarLayoutType == AppBarLayoutType.SEARCH_BAR) { + showSearchBar() + } else { + showToolbar() + } + initSystemBars() + } + } + + private fun showSearchBar() { + val layoutParams = binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams + binding.searchToolbar.visibility = View.VISIBLE + binding.searchText.text = getString(R.string.appbar_search_in, getString(R.string.nc_app_product_name)) + binding.conversationListToolbar.visibility = View.GONE + // layoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout + // .LayoutParams.SCROLL_FLAG_SNAP | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS); + layoutParams.scrollFlags = 0 + binding.conversationListAppbar.stateListAnimator = AnimatorInflater.loadStateListAnimator( + binding.conversationListAppbar.context, + R.animator.appbar_elevation_off + ) + binding.searchToolbar.layoutParams = layoutParams + } + + private fun showToolbar() { + val layoutParams = binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams + binding.searchToolbar.visibility = View.GONE + binding.conversationListToolbar.visibility = View.VISIBLE + viewThemeUtils.material.colorToolbarOverflowIcon(binding.conversationListToolbar) + layoutParams.scrollFlags = 0 + binding.conversationListAppbar.stateListAnimator = AnimatorInflater.loadStateListAnimator( + binding.conversationListAppbar.context, + R.animator.appbar_elevation_on + ) + binding.conversationListToolbar.layoutParams = layoutParams + } + + private fun hideSearchBar() { + val layoutParams = binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams + binding.searchToolbar.visibility = View.GONE + binding.conversationListToolbar.visibility = View.VISIBLE + layoutParams.scrollFlags = 0 + binding.conversationListAppbar.stateListAnimator = AnimatorInflater.loadStateListAnimator( + binding.conversationListAppbar.context, + R.animator.appbar_elevation_on + ) + } + + private fun hasActivityActionSendIntent(): Boolean = + Intent.ACTION_SEND == intent.action || Intent.ACTION_SEND_MULTIPLE == intent.action + + private fun showSearchView(searchView: SearchView?, searchItem: MenuItem?) { + binding.conversationListAppbar.stateListAnimator = AnimatorInflater.loadStateListAnimator( + binding.conversationListAppbar.context, + R.animator.appbar_elevation_on + ) + binding.conversationListToolbar.visibility = View.VISIBLE + binding.searchToolbar.visibility = View.GONE + searchItem!!.expandActionView() + } + + fun showSnackbar(text: String) { + Snackbar.make(binding.root, text, Snackbar.LENGTH_LONG).show() + } + + fun fetchRooms() { + conversationsListViewModel.getRooms() + } + + private fun fetchPendingInvitations() { + if (hasSpreedFeatureCapability(currentUser!!.capabilities!!.spreedCapability!!, SpreedFeatures.FEDERATION_V1)) { + binding.conversationListHintInclude.conversationListHintLayout.setOnClickListener { + val intent = Intent(this, InvitationsActivity::class.java) + startActivity(intent) + } + conversationsListViewModel.getFederationInvitations() + } + } + + private fun initOverallLayout(isConversationListNotEmpty: Boolean) { + if (isConversationListNotEmpty) { + if (binding.emptyLayout.visibility != View.GONE) { + binding.emptyLayout.visibility = View.GONE + } + if (binding.swipeRefreshLayoutView.visibility != View.VISIBLE) { + binding.swipeRefreshLayoutView.visibility = View.VISIBLE + } + } else { + if (binding.emptyLayout.visibility != View.VISIBLE) { + binding.emptyLayout.visibility = View.VISIBLE + } + if (binding.swipeRefreshLayoutView.visibility != View.GONE) { + binding.swipeRefreshLayoutView.visibility = View.GONE + } + } + } + + private fun addToConversationItems(conversation: ConversationModel) { + if (intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM) != null && + intent.getStringExtra(KEY_FORWARD_HIDE_SOURCE_ROOM) == conversation.token + ) { + return + } + + if (conversation.objectType == ConversationEnums.ObjectType.ROOM && + conversation.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY + ) { + return + } + + val headerTitle: String = resources!!.getString(R.string.conversations) + val genericTextHeaderItem: GenericTextHeaderItem + if (!callHeaderItems.containsKey(headerTitle)) { + genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils) + callHeaderItems[headerTitle] = genericTextHeaderItem + } + + val conversationItem = ConversationItem( + conversation, + currentUser!!, + this, + viewThemeUtils + ) + conversationItems.add(conversationItem) + val conversationItemWithHeader = ConversationItem( + conversation, + currentUser!!, + this, + callHeaderItems[headerTitle], + viewThemeUtils + ) + conversationItemsWithHeader.add(conversationItemWithHeader) + } + + private fun showErrorDialog() { + binding.floatingActionButton.let { + val dialogBuilder = MaterialAlertDialogBuilder(it.context) + .setIcon( + viewThemeUtils.dialog.colorMaterialAlertDialogIcon( + context, + R.drawable.ic_baseline_error_outline_24dp + ) + ) + .setTitle(R.string.error_loading_chats) + .setCancelable(false) + .setNegativeButton(R.string.close, null) + + if (resources!!.getBoolean(R.bool.multiaccount_support) && userManager.users.blockingGet().size > 1) { + dialogBuilder.setPositiveButton(R.string.nc_switch_account) { _, _ -> + val newFragment: DialogFragment = ChooseAccountDialogFragment.newInstance() + newFragment.show(supportFragmentManager, ChooseAccountDialogFragment.TAG) + } + } + + if (resources!!.getBoolean(R.bool.multiaccount_support)) { + dialogBuilder.setNeutralButton(R.string.nc_account_chooser_add_account) { _, _ -> + val intent = Intent(this, ServerSelectionActivity::class.java) + intent.putExtra(ADD_ADDITIONAL_ACCOUNT, true) + startActivity(intent) + } + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(it.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE), + dialog.getButton(AlertDialog.BUTTON_NEUTRAL) + ) + } + } + + private fun showNetworkErrorDialog(show: Boolean) { + binding.chatListConnectionLost.visibility = if (show) View.VISIBLE else View.GONE + } + + private fun showMaintenanceModeWarning(show: Boolean) { + binding.chatListMaintenanceWarning.visibility = if (show) View.VISIBLE else View.GONE + } + + private fun handleUI(show: Boolean) { + binding.floatingActionButton.isEnabled = show + binding.searchText.isEnabled = show + binding.searchText.isVisible = show + } + + private fun sortConversations(conversationItems: MutableList>) { + conversationItems.sortWith { o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> -> + val conversation1 = (o1 as ConversationItem).model + val conversation2 = (o2 as ConversationItem).model + CompareToBuilder() + .append(conversation2.favorite, conversation1.favorite) + .append(conversation2.lastActivity, conversation1.lastActivity) + .toComparison() + } + } + + private fun fetchOpenConversations(apiVersion: Int) { + searchableConversationItems.clear() + searchableConversationItems.addAll(conversationItemsWithHeader) + if (hasSpreedFeatureCapability( + currentUser!!.capabilities!!.spreedCapability!!, + SpreedFeatures.LISTABLE_ROOMS + ) + ) { + val openConversationItems: MutableList> = ArrayList() + openConversationsQueryDisposable = ncApi.getOpenConversations( + credentials, + ApiUtils.getUrlForOpenConversations(apiVersion, currentUser!!.baseUrl!!), + "" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ (ocs): RoomsOverall -> + for (conversation in ocs!!.data!!) { + val headerTitle = resources!!.getString(R.string.openConversations) + var genericTextHeaderItem: GenericTextHeaderItem + if (!callHeaderItems.containsKey(headerTitle)) { + genericTextHeaderItem = GenericTextHeaderItem(headerTitle, viewThemeUtils) + callHeaderItems[headerTitle] = genericTextHeaderItem + } + val conversationItem = ConversationItem( + ConversationModel.mapToConversationModel(conversation, currentUser!!), + currentUser!!, + this, + callHeaderItems[headerTitle], + viewThemeUtils + ) + openConversationItems.add(conversationItem) + } + searchableConversationItems.addAll(openConversationItems) + }, { throwable: Throwable -> + Log.e(TAG, "fetchData - getRooms - ERROR", throwable) + handleHttpExceptions(throwable) + dispose(openConversationsQueryDisposable) + }) { dispose(openConversationsQueryDisposable) } + } else { + Log.d(TAG, "no open conversations fetched because of missing capability") + } + } + + private fun fetchUsers(query: String = "") { + contactsViewModel.getContactsFromSearchParams(query) + } + + private fun handleHttpExceptions(throwable: Throwable) { + if (!networkMonitor.isOnline.value) return + + if (throwable is HttpException) { + when (throwable.code()) { + HTTP_UNAUTHORIZED -> showUnauthorizedDialog() + HTTP_CLIENT_UPGRADE_REQUIRED -> showOutdatedClientDialog() + HTTP_SERVICE_UNAVAILABLE -> showServiceUnavailableDialog(throwable) + else -> { + Log.e(TAG, "Http Exception in ConversationListActivity", throwable) + showErrorDialog() + } + } + } else { + Log.e(TAG, "Exception in ConversationListActivity", throwable) + showErrorDialog() + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun prepareViews() { + hideLogoForBrandedClients() + + showMaintenanceModeWarning(false) + + layoutManager = SmoothScrollLinearLayoutManager(this) + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.adapter = adapter + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + val isSearchActive = searchBehaviorSubject.value + if (!isSearchActive!!) { + checkToShowUnreadBubble() + } + } + } + }) + binding.recyclerView.setOnTouchListener { v: View, _: MotionEvent? -> + if (!isDestroyed) { + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v.windowToken, 0) + } + false + } + binding.swipeRefreshLayoutView.setOnRefreshListener { + showMaintenanceModeWarning(false) + fetchRooms() + fetchPendingInvitations() + } + binding.swipeRefreshLayoutView.let { viewThemeUtils.androidx.themeSwipeRefreshLayout(it) } + binding.emptyLayout.setOnClickListener { showNewConversationsScreen() } + binding.floatingActionButton.setOnClickListener { + run(context) + showNewConversationsScreen() + } + binding.floatingActionButton.let { viewThemeUtils.material.themeFAB(it) } + + binding.switchAccountButton.setOnClickListener { + if (resources != null && resources!!.getBoolean(R.bool.multiaccount_support)) { + val newFragment: DialogFragment = ChooseAccountDialogFragment.newInstance() + newFragment.show(supportFragmentManager, ChooseAccountDialogFragment.TAG) + } else { + val intent = Intent(context, SettingsActivity::class.java) + startActivity(intent) + } + } + + updateFilterConversationButtonColor() + + binding.filterConversationsButton.setOnClickListener { + val newFragment = FilterConversationFragment.newInstance(filterState) + newFragment.show(supportFragmentManager, FilterConversationFragment.TAG) + } + + binding.threadsButton.setOnClickListener { + openFollowedThreadsOverview() + } + binding.threadsButton.let { + viewThemeUtils.platform.colorImageView(it, ColorRole.ON_SURFACE_VARIANT) + } + + binding.newMentionPopupBubble.visibility = View.GONE + binding.newMentionPopupBubble.setOnClickListener { + val layoutManager = binding.recyclerView.layoutManager as SmoothScrollLinearLayoutManager? + layoutManager?.scrollToPositionWithOffset( + nextUnreadConversationScrollPosition, + binding.recyclerView.height / OFFSET_HEIGHT_DIVIDER + ) + binding.newMentionPopupBubble.visibility = View.GONE + } + binding.newMentionPopupBubble.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it) } + } + + private fun hideLogoForBrandedClients() { + if (!BrandingUtils.isOriginalNextcloudClient(applicationContext)) { + binding.emptyListIcon.visibility = View.GONE + } + } + + @SuppressLint("CheckResult") + @Suppress("Detekt.TooGenericExceptionCaught") + private fun checkToShowUnreadBubble() { + searchBehaviorSubject.subscribe { value -> + if (value) { + nextUnreadConversationScrollPosition = 0 + binding.newMentionPopupBubble.visibility = View.GONE + } else { + try { + val lastVisibleItem = layoutManager!!.findLastCompletelyVisibleItemPosition() + for (flexItem in conversationItems) { + val conversation: ConversationModel = (flexItem as ConversationItem).model + val position = adapter?.getGlobalPositionOf(flexItem) + if (position != null && hasUnreadItems(conversation) && position > lastVisibleItem) { + nextUnreadConversationScrollPosition = position + if (!binding.newMentionPopupBubble.isShown) { + binding.newMentionPopupBubble.visibility = View.VISIBLE + val popupAnimation = AnimationUtils.loadAnimation(this, R.anim.popup_animation) + binding.newMentionPopupBubble.startAnimation(popupAnimation) + } + return@subscribe + } + } + nextUnreadConversationScrollPosition = 0 + binding.newMentionPopupBubble.visibility = View.GONE + } catch (e: NullPointerException) { + Log.d( + TAG, + "A NPE was caught when trying to show the unread popup bubble. This might happen when the " + + "user already left the conversations-list screen so the popup bubble is not available " + + "anymore.", + e + ) + } + } + } + } + + private fun hasUnreadItems(conversation: ConversationModel) = + conversation.unreadMention || + conversation.unreadMessages > 0 && + conversation.type === ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + + private fun showNewConversationsScreen() { + val intent = Intent(context, ContactsActivity::class.java) + startActivity(intent) + } + + private fun dispose(disposable: Disposable?) { + if (disposable != null && !disposable.isDisposed) { + disposable.dispose() + } else if (disposable == null && roomsQueryDisposable != null && !roomsQueryDisposable!!.isDisposed) { + roomsQueryDisposable!!.dispose() + roomsQueryDisposable = null + } else if (disposable == null && + openConversationsQueryDisposable != null && + !openConversationsQueryDisposable!!.isDisposed + ) { + openConversationsQueryDisposable!!.dispose() + openConversationsQueryDisposable = null + } + } + + override fun onSaveInstanceState(bundle: Bundle) { + super.onSaveInstanceState(bundle) + + if (searchView != null && !TextUtils.isEmpty(searchView!!.query)) { + bundle.putString(KEY_SEARCH_QUERY, searchView!!.query.toString()) + } + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + + if (savedInstanceState.containsKey(KEY_SEARCH_QUERY)) { + searchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY, "") + } + } + + public override fun onDestroy() { + super.onDestroy() + dispose(null) + if (searchViewDisposable != null && !searchViewDisposable!!.isDisposed) { + searchViewDisposable!!.dispose() + } + } + + private fun onQueryTextChange(newText: String?) { + if (!TextUtils.isEmpty(searchQuery)) { + val filter = searchQuery + searchQuery = "" + performFilterAndSearch(filter) + } else if (adapter?.hasNewFilter(newText) == true) { + performFilterAndSearch(newText) + } + } + + private fun performFilterAndSearch(filter: String?) { + if (filter!!.length >= SEARCH_MIN_CHARS) { + clearMessageSearchResults() + binding.noArchivedConversationLayout.visibility = View.GONE + + fetchUsers(filter) + + if (hasFilterEnabled()) { + adapter?.updateDataSet(conversationItems) + adapter?.setFilter(filter) + adapter?.filterItems() + adapter?.updateDataSet(filterableConversationItems) + } else { + adapter?.updateDataSet(searchableConversationItems) + adapter?.setFilter(filter) + adapter?.filterItems() + } + + if (isUnifiedSearchAvailable(currentUser!!.capabilities!!.spreedCapability!!)) { + startMessageSearch(filter) + } + } else { + resetSearchResults() + } + } + + private fun resetSearchResults() { + clearMessageSearchResults() + adapter?.updateDataSet(conversationItems) + adapter?.setFilter("") + adapter?.filterItems() + val archiveFilterOn = filterState[ARCHIVE] == true + if (archiveFilterOn && adapter!!.isEmpty) { + binding.noArchivedConversationLayout.visibility = View.VISIBLE + } else { + binding.noArchivedConversationLayout.visibility = View.GONE + } + } + + private fun clearMessageSearchResults() { + val firstHeader = adapter?.getSectionHeader(0) + if (firstHeader != null && firstHeader.itemViewType == MessagesTextHeaderItem.VIEW_TYPE) { + adapter?.removeSection(firstHeader) + } else { + adapter?.removeItemsOfType(MessageResultItem.VIEW_TYPE) + adapter?.removeItemsOfType(MessagesTextHeaderItem.VIEW_TYPE) + } + adapter?.removeItemsOfType(LoadMoreResultsItem.VIEW_TYPE) + } + + @SuppressLint("CheckResult") // handled by helper + private fun startMessageSearch(search: String?) { + binding.swipeRefreshLayoutView.isRefreshing = true + searchHelper?.startMessageSearch(search!!) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe({ results: MessageSearchResults -> onMessageSearchResult(results) }) { throwable: Throwable -> + onMessageSearchError( + throwable + ) + } + } + + @SuppressLint("CheckResult") // handled by helper + private fun loadMoreMessages() { + binding.swipeRefreshLayoutView.isRefreshing = true + val observable = searchHelper!!.loadMore() + observable?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe({ results: MessageSearchResults -> onMessageSearchResult(results) }) { throwable: Throwable -> + onMessageSearchError( + throwable + ) + } + } + + override fun onItemClick(view: View, position: Int): Boolean { + val item = adapter?.getItem(position) + if (item != null) { + when (item) { + is MessageResultItem -> { + val token = item.messageEntry.conversationToken + val conversationName = ( + conversationItems.first { + (it is ConversationItem) && it.model.token == token + } as ConversationItem + ).model.displayName + + binding.genericComposeView.apply { + val shouldDismiss = mutableStateOf(false) + setContent { + val bundle = bundleOf() + bundle.putString(BundleKeys.KEY_CREDENTIALS, credentials!!) + bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl) + bundle.putString(KEY_ROOM_TOKEN, token) + bundle.putString(BundleKeys.KEY_MESSAGE_ID, item.messageEntry.messageId) + bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, conversationName) + ContextChatCompose(bundle).GetDialogView(shouldDismiss, context) + } + } + } + + is LoadMoreResultsItem -> { + loadMoreMessages() + } + + is ConversationItem -> { + handleConversation(item.model) + } + + is ContactItem -> { + contactsViewModel.createRoom( + ROOM_TYPE_ONE_ONE, + null, + item.model.actorId!!, + null + ) + } + } + } + return true + } + + private fun showConversationByToken(conversationToken: String) { + for (absItem in conversationItems) { + val conversationItem = absItem as ConversationItem + if (conversationItem.model.token == conversationToken) { + val conversation = conversationItem.model + handleConversation(conversation) + } + } + } + + @Suppress("Detekt.ComplexMethod") + private fun handleConversation(conversation: ConversationModel?) { + selectedConversation = conversation + if (selectedConversation != null) { + val hasChatPermission = ParticipantPermissions( + currentUser!!.capabilities!!.spreedCapability!!, + selectedConversation!! + ) + .hasChatPermission() + if (showShareToScreen) { + if (hasChatPermission && + !isReadOnlyConversation(selectedConversation!!) && + !shouldShowLobby(selectedConversation!!) + ) { + handleSharedData() + } else { + Snackbar.make(binding.root, R.string.send_to_forbidden, Snackbar.LENGTH_LONG).show() + } + } else if (forwardMessage) { + if (hasChatPermission && !isReadOnlyConversation(selectedConversation!!)) { + openConversation(intent.getStringExtra(KEY_FORWARD_MSG_TEXT)) + forwardMessage = false + } else { + Snackbar.make(binding.root, R.string.send_to_forbidden, Snackbar.LENGTH_LONG).show() + } + } else { + openConversation() + } + } + } + + private fun shouldShowLobby(conversation: ConversationModel): Boolean { + val participantPermissions = ParticipantPermissions( + currentUser!!.capabilities?.spreedCapability!!, + selectedConversation!! + ) + return conversation.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY && + !ConversationUtils.canModerate(conversation, currentUser!!.capabilities!!.spreedCapability!!) && + !participantPermissions.canIgnoreLobby() + } + + private fun isReadOnlyConversation(conversation: ConversationModel): Boolean = + conversation.conversationReadOnlyState === + ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY + + private fun handleSharedData() { + collectDataFromIntent() + if (textToPaste!!.isNotEmpty()) { + openConversation(textToPaste) + } else if (filesToShare != null && filesToShare!!.isNotEmpty()) { + showSendFilesConfirmDialog() + } else { + Snackbar + .make(binding.root, context.resources.getString(R.string.nc_common_error_sorry), Snackbar.LENGTH_LONG) + .show() + } + } + + private fun showSendFilesConfirmDialog() { + if (platformPermissionUtil.isFilesPermissionGranted()) { + val fileNamesWithLineBreaks = StringBuilder("\n") + for (file in filesToShare!!) { + val filename = FileUtils.getFileName(file.toUri(), context) + fileNamesWithLineBreaks.append(filename).append("\n") + } + val confirmationQuestion: String = if (filesToShare!!.size == 1) { + String.format( + resources!!.getString(R.string.nc_upload_confirm_send_single), + selectedConversation!!.displayName + ) + } else { + String.format( + resources!!.getString(R.string.nc_upload_confirm_send_multiple), + selectedConversation!!.displayName + ) + } + binding.floatingActionButton.let { + val dialogBuilder = MaterialAlertDialogBuilder(it.context) + .setIcon(viewThemeUtils.dialog.colorMaterialAlertDialogIcon(context, R.drawable.upload)) + .setTitle(confirmationQuestion) + .setMessage(fileNamesWithLineBreaks.toString()) + .setPositiveButton(R.string.nc_yes) { _, _ -> + upload() + openConversation() + } + .setNegativeButton(R.string.nc_no) { _, _ -> + Log.d(TAG, "sharing files aborted, going back to share-to screen") + } + + viewThemeUtils.dialog + .colorMaterialAlertDialogBackground(it.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } else { + UploadAndShareFilesWorker.requestStoragePermission(this) + } + } + + private fun clearIntentAction() { + intent.action = "" + } + + override fun onItemLongClick(position: Int) { + this.lifecycleScope.launch { + if (showShareToScreen || !networkMonitor.isOnline.value) { + Log.d(TAG, "sharing to multiple rooms not yet implemented. onItemLongClick is ignored.") + } else { + val clickedItem: Any? = adapter?.getItem(position) + if (clickedItem != null && clickedItem is ConversationItem) { + val conversation = clickedItem.model + conversationsListBottomDialog = ConversationsListBottomDialog( + this@ConversationsListActivity, + currentUser!!, + conversation + ) + conversationsListBottomDialog!!.show() + } + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun collectDataFromIntent() { + filesToShare = ArrayList() + if (intent != null) { + if (Intent.ACTION_SEND == intent.action || Intent.ACTION_SEND_MULTIPLE == intent.action) { + try { + val mimeType = intent.type + if (Mimetype.TEXT_PLAIN == mimeType && intent.getStringExtra(Intent.EXTRA_TEXT) != null) { + // Share from Google Chrome sets text/plain MIME type, but also provides a content:// URI + // with a *screenshot* of the current page in getClipData(). + // Here we assume that when sharing a web page the user would prefer to send the URL + // of the current page rather than a screenshot. + textToPaste = intent.getStringExtra(Intent.EXTRA_TEXT) + } else { + if (intent.clipData != null) { + for (i in 0 until intent.clipData!!.itemCount) { + val item = intent.clipData!!.getItemAt(i) + if (item.uri != null) { + filesToShare!!.add(item.uri.toString()) + } else if (item.text != null) { + textToPaste = item.text.toString() + break + } else { + Log.w(TAG, "datatype not yet implemented for share-to") + } + } + } else { + filesToShare!!.add(intent.data.toString()) + } + } + if (filesToShare!!.isEmpty() && textToPaste!!.isEmpty()) { + Snackbar.make( + binding.root, + context.resources.getString(R.string.nc_common_error_sorry), + Snackbar.LENGTH_LONG + ).show() + Log.e(TAG, "failed to get data from intent") + } + } catch (e: Exception) { + Snackbar.make( + binding.root, + context.resources.getString(R.string.nc_common_error_sorry), + Snackbar.LENGTH_LONG + ).show() + Log.e(TAG, "Something went wrong when extracting data from intent") + } + } + } + } + + private fun upload() { + if (selectedConversation == null) { + Snackbar.make( + binding.root, + context.resources.getString(R.string.nc_common_error_sorry), + Snackbar.LENGTH_LONG + ).show() + Log.e(TAG, "not able to upload any files because conversation was null.") + return + } + try { + filesToShare?.forEach { + UploadAndShareFilesWorker.upload( + it, + selectedConversation!!.token, + selectedConversation!!.displayName, + null + ) + } + } catch (e: IllegalArgumentException) { + Snackbar.make(binding.root, context.resources.getString(R.string.nc_upload_failed), Snackbar.LENGTH_LONG) + .show() + Log.e(TAG, "Something went wrong when trying to upload file", e) + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + when (requestCode) { + UploadAndShareFilesWorker.REQUEST_PERMISSION -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "upload starting after permissions were granted") + showSendFilesConfirmDialog() + } else { + Snackbar.make( + binding.root, + context.getString(R.string.read_storage_no_permission), + Snackbar.LENGTH_LONG + ).show() + } + } + + REQUEST_POST_NOTIFICATIONS_PERMISSION -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Notification permission was granted") + + if (!PowerManagerUtils().isIgnoringBatteryOptimizations() && + ClosedInterfaceImpl().isGooglePlayServicesAvailable + ) { + val dialogText = String.format( + context.resources.getString(R.string.nc_ignore_battery_optimization_dialog_text), + context.resources.getString(R.string.nc_app_name) + ) + + val dialogBuilder = MaterialAlertDialogBuilder(this) + .setTitle(R.string.nc_ignore_battery_optimization_dialog_title) + .setMessage(dialogText) + .setPositiveButton(R.string.nc_ok) { _, _ -> + startActivity( + Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + ) + } + .setNegativeButton(R.string.nc_common_dismiss, null) + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } else { + Log.d( + TAG, + "Notification permission is denied. Either because user denied it when being asked. " + + "Or permission is already denied and android decided to not offer the dialog." + ) + } + } + } + } + + private fun showNotificationWarning() { + if (shouldShowNotificationWarning()) { + binding.conversationListNotificationWarning.conversationListNotificationWarningLayout.visibility = + View.VISIBLE + binding.conversationListNotificationWarning.notNowButton.setOnClickListener { + binding.conversationListNotificationWarning.conversationListNotificationWarningLayout.visibility = + View.GONE + val lastWarningDate = System.currentTimeMillis() + appPreferences.setNotificationWarningLastPostponedDate(lastWarningDate) + } + binding.conversationListNotificationWarning.showSettingsButton.setOnClickListener { + val bundle = Bundle() + bundle.putBoolean(KEY_SCROLL_TO_NOTIFICATION_CATEGORY, true) + val settingsIntent = Intent(context, SettingsActivity::class.java) + settingsIntent.putExtras(bundle) + startActivity(settingsIntent) + } + } else { + binding.conversationListNotificationWarning.conversationListNotificationWarningLayout.visibility = View.GONE + } + } + + private fun shouldShowNotificationWarning(): Boolean { + fun shouldShowWarningIfDateTooOld(date1: Long): Boolean { + val currentTimeMillis = System.currentTimeMillis() + val differenceMillis = currentTimeMillis - date1 + val daysForWarningInMillis = TimeUnit.DAYS.toMillis(DAYS_FOR_NOTIFICATION_WARNING) + return differenceMillis > daysForWarningInMillis + } + + fun shouldShowNotificationWarningByUserChoice(): Boolean { + if (appPreferences.showRegularNotificationWarning) { + val lastWarningDate = appPreferences.getNotificationWarningLastPostponedDate() + return if (lastWarningDate == NOTIFICATION_WARNING_DATE_NOT_SET) { + true + } else { + shouldShowWarningIfDateTooOld(lastWarningDate) + } + } else { + return false + } + } + + val notificationPermissionNotGranted = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + !platformPermissionUtil.isPostNotificationsPermissionGranted() + val batteryOptimizationNotIgnored = !PowerManagerUtils().isIgnoringBatteryOptimizations() + + val messagesChannelNotEnabled = !NotificationUtils.isMessagesNotificationChannelEnabled(this) + val callsChannelNotEnabled = !NotificationUtils.isCallsNotificationChannelEnabled(this) + + val serverNotificationAppInstalled = + currentUser?.capabilities?.notificationsCapability?.features?.isNotEmpty() == true + + val settingsOfUserAreWrong = notificationPermissionNotGranted || + batteryOptimizationNotIgnored || + messagesChannelNotEnabled || + callsChannelNotEnabled || + !serverNotificationAppInstalled + + return settingsOfUserAreWrong && + shouldShowNotificationWarningByUserChoice() && + ClosedInterfaceImpl().isGooglePlayServicesAvailable + } + + private fun openConversation(textToPaste: String? = "") { + if (CallActivity.active && + selectedConversation!!.token != ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken + ) { + Snackbar.make( + binding.root, + context.getString(R.string.restrict_join_other_room_while_call), + Snackbar.LENGTH_LONG + ).show() + return + } + + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, selectedConversation!!.token) + bundle.putString(KEY_SHARED_TEXT, textToPaste) + if (selectedMessageId != null) { + bundle.putString(BundleKeys.KEY_MESSAGE_ID, selectedMessageId) + selectedMessageId = null + } + + val intent = Intent(context, ChatActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + + clearIntentAction() + } + + @Subscribe(sticky = true, threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(eventStatus: EventStatus) { + if (currentUser != null && eventStatus.userId == currentUser!!.id) { + when (eventStatus.eventType) { + EventStatus.EventType.CONVERSATION_UPDATE -> if (eventStatus.isAllGood && !isRefreshing) { + fetchRooms() + } + + else -> {} + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(conversationsListFetchDataEvent: ConversationsListFetchDataEvent?) { + fetchRooms() + Handler().postDelayed({ + if (conversationsListBottomDialog!!.isShowing) { + conversationsListBottomDialog!!.dismiss() + } + }, BOTTOM_SHEET_DELAY) + } + + fun showDeleteConversationDialog(conversation: ConversationModel) { + binding.floatingActionButton.let { + val dialogBuilder = MaterialAlertDialogBuilder(it.context) + .setIcon( + viewThemeUtils.dialog + .colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp) + ) + .setTitle(R.string.nc_delete_call) + .setMessage(R.string.nc_delete_conversation_more) + .setPositiveButton(R.string.nc_delete) { _, _ -> + deleteConversation(conversation) + } + .setNegativeButton(R.string.nc_cancel) { _, _ -> + } + + viewThemeUtils.dialog + .colorMaterialAlertDialogBackground(it.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } + + private fun showUnauthorizedDialog() { + binding.floatingActionButton.let { + val dialogBuilder = MaterialAlertDialogBuilder(it.context) + .setIcon( + viewThemeUtils.dialog.colorMaterialAlertDialogIcon( + context, + R.drawable.ic_delete_black_24dp + ) + ) + .setTitle(R.string.nc_dialog_invalid_password) + .setMessage(R.string.nc_dialog_reauth_or_delete) + .setCancelable(false) + .setPositiveButton(R.string.nc_settings_remove_account) { _, _ -> + deleteUserAndRestartApp() + } + .setNegativeButton(R.string.nc_settings_reauthorize) { _, _ -> + val intent = Intent(context, WebViewLoginActivity::class.java) + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl!!) + bundle.putBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT, true) + intent.putExtras(bundle) + startActivity(intent) + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(it.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } + + @SuppressLint("CheckResult") + private fun deleteUserAndRestartApp() { + userManager.scheduleUserForDeletionWithId(currentUser!!.id!!).blockingGet() + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo? -> + + when (workInfo?.state) { + WorkInfo.State.SUCCEEDED -> { + val text = String.format( + context.resources.getString(R.string.nc_deleted_user), + currentUser!!.displayName + ) + Toast.makeText( + context, + text, + Toast.LENGTH_LONG + ).show() + restartApp() + } + + WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "something went wrong when deleting user with id " + currentUser!!.userId) + restartApp() + } + + else -> {} + } + } + } + + private fun restartApp() { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + + private fun showOutdatedClientDialog() { + binding.floatingActionButton.let { + val dialogBuilder = MaterialAlertDialogBuilder(it.context) + .setIcon( + viewThemeUtils.dialog.colorMaterialAlertDialogIcon( + context, + R.drawable.ic_info_white_24dp + ) + ) + .setTitle(R.string.nc_dialog_outdated_client) + .setMessage(R.string.nc_dialog_outdated_client_description) + .setCancelable(false) + .setPositiveButton(R.string.nc_dialog_outdated_client_option_update) { _, _ -> + try { + startActivity( + Intent(Intent.ACTION_VIEW, (CLIENT_UPGRADE_MARKET_LINK + packageName).toUri()) + ) + } catch (e: ActivityNotFoundException) { + startActivity( + Intent(Intent.ACTION_VIEW, (CLIENT_UPGRADE_GPLAY_LINK + packageName).toUri()) + ) + } + } + + if (resources!!.getBoolean(R.bool.multiaccount_support) && userManager.users.blockingGet().size > 1) { + dialogBuilder.setNegativeButton(R.string.nc_switch_account) { _, _ -> + val newFragment: DialogFragment = ChooseAccountDialogFragment.newInstance() + newFragment.show(supportFragmentManager, ChooseAccountDialogFragment.TAG) + } + } + + if (resources!!.getBoolean(R.bool.multiaccount_support)) { + dialogBuilder.setNeutralButton(R.string.nc_account_chooser_add_account) { _, _ -> + val intent = Intent(this, ServerSelectionActivity::class.java) + intent.putExtra(ADD_ADDITIONAL_ACCOUNT, true) + startActivity(intent) + } + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(it.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE), + dialog.getButton(AlertDialog.BUTTON_NEUTRAL) + ) + } + } + + private fun showServiceUnavailableDialog(httpException: HttpException) { + if (httpException.response()?.headers()?.get(MAINTENANCE_MODE_HEADER_KEY) == "1") { + showMaintenanceModeWarning(true) + } else { + showErrorDialog() + } + } + + private fun showServerEOLDialog() { + binding.floatingActionButton.let { + val dialogBuilder = MaterialAlertDialogBuilder(it.context) + .setIcon(viewThemeUtils.dialog.colorMaterialAlertDialogIcon(context, R.drawable.ic_warning_white)) + .setTitle(R.string.nc_settings_server_eol_title) + .setMessage(R.string.nc_settings_server_eol) + .setCancelable(false) + .setPositiveButton(R.string.nc_settings_remove_account) { _, _ -> + deleteUserAndRestartApp() + } + + if (resources!!.getBoolean(R.bool.multiaccount_support) && userManager.users.blockingGet().size > 1) { + dialogBuilder.setNegativeButton(R.string.nc_switch_account) { _, _ -> + val newFragment: DialogFragment = ChooseAccountDialogFragment.newInstance() + newFragment.show(supportFragmentManager, ChooseAccountDialogFragment.TAG) + } + } + + if (resources!!.getBoolean(R.bool.multiaccount_support)) { + dialogBuilder.setNeutralButton(R.string.nc_account_chooser_add_account) { _, _ -> + val intent = Intent(this, ServerSelectionActivity::class.java) + intent.putExtra(ADD_ADDITIONAL_ACCOUNT, true) + startActivity(intent) + } + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(it.context, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE), + dialog.getButton(AlertDialog.BUTTON_NEUTRAL) + ) + } + } + + private fun deleteConversation(conversation: ConversationModel) { + val data = Data.Builder() + data.putLong( + KEY_INTERNAL_USER_ID, + currentUser?.id!! + ) + data.putString(KEY_ROOM_TOKEN, conversation.token) + + val deleteConversationWorker = + OneTimeWorkRequest.Builder(DeleteConversationWorker::class.java).setInputData(data.build()).build() + WorkManager.getInstance().enqueue(deleteConversationWorker) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(deleteConversationWorker.id) + .observeForever { workInfo: WorkInfo? -> + if (workInfo != null) { + when (workInfo.state) { + WorkInfo.State.SUCCEEDED -> { + showSnackbar( + String.format( + context.resources.getString(R.string.deleted_conversation), + conversation.displayName + ) + ) + } + + WorkInfo.State.FAILED -> { + showSnackbar(context.resources.getString(R.string.nc_common_error_sorry)) + } + + else -> { + } + } + } + } + } + + private fun onMessageSearchResult(results: MessageSearchResults) { + if (searchView!!.query.isNotEmpty()) { + clearMessageSearchResults() + val entries = results.messages + if (entries.isNotEmpty()) { + val adapterItems: MutableList> = ArrayList(entries.size + 1) + + for (i in entries.indices) { + val showHeader = i == 0 + adapterItems.add( + MessageResultItem( + context, + currentUser!!, + entries[i], + showHeader, + viewThemeUtils = viewThemeUtils + ) + ) + } + + if (results.hasMore) { + adapterItems.add(LoadMoreResultsItem) + } + + adapter?.addItems(Int.MAX_VALUE, adapterItems) + binding.recyclerView.scrollToPosition(0) + } + } + binding.swipeRefreshLayoutView.isRefreshing = false + } + + private fun onMessageSearchError(throwable: Throwable) { + handleHttpExceptions(throwable) + binding.swipeRefreshLayoutView.isRefreshing = false + } + + fun updateFilterState(mention: Boolean, unread: Boolean) { + filterState[MENTION] = mention + filterState[UNREAD] = unread + } + + fun setFilterableItems(items: MutableList>) { + filterableConversationItems = items + } + + fun updateFilterConversationButtonColor() { + if (hasFilterEnabled()) { + binding.filterConversationsButton.let { viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) } + } else { + binding.filterConversationsButton.let { + viewThemeUtils.platform.colorImageView( + it, + ColorRole.ON_SURFACE_VARIANT + ) + } + } + } + + fun openFollowedThreadsOverview() { + val threadsUrl = ApiUtils.getUrlForSubscribedThreads( + version = 1, + baseUrl = currentUser!!.baseUrl + ) + + val bundle = Bundle() + bundle.putString(ThreadsOverviewActivity.KEY_APPBAR_TITLE, getString(R.string.followed_threads)) + bundle.putString(ThreadsOverviewActivity.KEY_THREADS_SOURCE_URL, threadsUrl) + val threadsOverviewIntent = Intent(context, ThreadsOverviewActivity::class.java) + threadsOverviewIntent.putExtras(bundle) + startActivity(threadsOverviewIntent) + } + + companion object { + private val TAG = ConversationsListActivity::class.java.simpleName + const val UNREAD_BUBBLE_DELAY = 2500 + const val BOTTOM_SHEET_DELAY: Long = 2500 + private const val KEY_SEARCH_QUERY = "ConversationsListActivity.searchQuery" + private const val CHAT_ACTIVITY_LOCAL_NAME = "com.nextcloud.talk.chat.ChatActivity" + const val SEARCH_DEBOUNCE_INTERVAL_MS = 300 + const val SEARCH_MIN_CHARS = 1 + const val HTTP_UNAUTHORIZED = 401 + const val HTTP_CLIENT_UPGRADE_REQUIRED = 426 + const val CLIENT_UPGRADE_MARKET_LINK = "market://details?id=" + const val CLIENT_UPGRADE_GPLAY_LINK = "https://play.google.com/store/apps/details?id=" + const val HTTP_SERVICE_UNAVAILABLE = 503 + const val MAINTENANCE_MODE_HEADER_KEY = "X-Nextcloud-Maintenance-Mode" + const val REQUEST_POST_NOTIFICATIONS_PERMISSION = 111 + const val BADGE_OFFSET = 35 + const val DAYS_FOR_NOTIFICATION_WARNING = 5L + const val NOTIFICATION_WARNING_DATE_NOT_SET = 0L + const val OFFSET_HEIGHT_DIVIDER: Int = 3 + const val ROOM_TYPE_ONE_ONE = "1" + private const val SIXTEEN_HOURS_IN_SECONDS: Long = 57600 + const val LONG_1000: Long = 1000 + private const val NOTE_TO_SELF_SHORTCUT_ID = "NOTE_TO_SELF_SHORTCUT_ID" + private const val CONVERSATION_ITEM_HEIGHT = 44 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt new file mode 100644 index 0000000..a115ac0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/OfflineConversationsRepository.kt @@ -0,0 +1,43 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationlist.data + +import com.nextcloud.talk.models.domain.ConversationModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow + +interface OfflineConversationsRepository { + + /** + * Stream of a list of rooms, for use in the conversation list. + */ + val roomListFlow: Flow> + + /** + * Stream of a single conversation, for use in each conversations settings. + */ + val conversationFlow: Flow + + /** + * Loads rooms from local storage. If the rooms are not found, then it + * synchronizes the database with the server, before retrying exactly once. Only + * emits to [roomListFlow] if the rooms list is not empty. + * + */ + fun getRooms(): Job + + /** + * Called once onStart to emit a conversation to [conversationFlow] + * to be handled asynchronously. + */ + fun getRoom(roomToken: String): Job + + suspend fun updateConversation(conversationModel: ConversationModel) + + suspend fun getLocallyStoredConversation(roomToken: String): ConversationModel? +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/ConversationsNetworkDataSource.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/ConversationsNetworkDataSource.kt new file mode 100644 index 0000000..bf3ae39 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/ConversationsNetworkDataSource.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationlist.data.network + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.conversations.Conversation +import io.reactivex.Observable + +interface ConversationsNetworkDataSource { + fun getRooms(user: User, url: String, includeStatus: Boolean): Observable> +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt new file mode 100644 index 0000000..2e3687a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/OfflineFirstConversationsRepository.kt @@ -0,0 +1,170 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationlist.data.network + +import android.util.Log +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository +import com.nextcloud.talk.data.database.dao.ConversationsDao +import com.nextcloud.talk.data.database.mappers.asEntity +import com.nextcloud.talk.data.database.mappers.asModel +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.data.network.NetworkMonitor +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.utils.CapabilitiesUtil.isUserStatusAvailable +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import javax.inject.Inject + +class OfflineFirstConversationsRepository @Inject constructor( + private val dao: ConversationsDao, + private val network: ConversationsNetworkDataSource, + private val chatNetworkDataSource: ChatNetworkDataSource, + private val networkMonitor: NetworkMonitor, + private val currentUserProviderNew: CurrentUserProviderNew +) : OfflineConversationsRepository { + override val roomListFlow: Flow> + get() = _roomListFlow + private val _roomListFlow: MutableSharedFlow> = MutableSharedFlow() + + override val conversationFlow: Flow + get() = _conversationFlow + private val _conversationFlow: MutableSharedFlow = MutableSharedFlow() + + private val scope = CoroutineScope(Dispatchers.IO) + private var user: User = currentUserProviderNew.currentUser.blockingGet() + + override fun getRooms(): Job = + scope.launch { + val initialConversationModels = getListOfConversations(user.id!!) + _roomListFlow.emit(initialConversationModels) + + if (networkMonitor.isOnline.value) { + val conversationEntitiesFromSync = getRoomsFromServer() + if (!conversationEntitiesFromSync.isNullOrEmpty()) { + val conversationModelsFromSync = conversationEntitiesFromSync.map(ConversationEntity::asModel) + _roomListFlow.emit(conversationModelsFromSync) + } + } + } + + override fun getRoom(roomToken: String): Job = + scope.launch { + chatNetworkDataSource.getRoom(user, roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(p0: Disposable) { + // unused atm + } + + override fun onError(e: Throwable) { + runBlocking { + // In case network is offline or call fails + val id = user.id!! + val model = getConversation(id, roomToken) + if (model != null) { + _conversationFlow.emit(model) + } else { + Log.e(TAG, "Conversation model not found on device database") + } + } + } + + override fun onComplete() { + // unused atm + } + + override fun onNext(model: ConversationModel) { + runBlocking { + _conversationFlow.emit(model) + val entityList = listOf(model.asEntity()) + dao.upsertConversations(user.id!!, entityList) + } + } + }) + } + + override suspend fun updateConversation(conversationModel: ConversationModel) { + val entity = conversationModel.asEntity() + dao.updateConversation(entity) + } + + override suspend fun getLocallyStoredConversation(roomToken: String): ConversationModel? { + val id = user.id!! + return getConversation(id, roomToken) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private suspend fun getRoomsFromServer(): List? { + var conversationsFromSync: List? = null + + if (!networkMonitor.isOnline.value) { + Log.d(TAG, "Device is offline, can't load conversations from server") + return null + } + + val includeStatus = isUserStatusAvailable(user) + + try { + val conversationsList = network.getRooms(user, user.baseUrl!!, includeStatus) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .blockingSingle() + + conversationsFromSync = conversationsList.map { + it.asEntity(user.id!!) + } + + deleteLeftConversations(conversationsFromSync) + dao.upsertConversations(user.id!!, conversationsFromSync) + } catch (e: Exception) { + Log.e(TAG, "Something went wrong when fetching conversations", e) + } + return conversationsFromSync + } + + private suspend fun deleteLeftConversations(conversationsFromSync: List) { + val conversationsFromSyncIds = conversationsFromSync.map { it.internalId }.toSet() + val oldConversationsFromDb = dao.getConversationsForUser(user.id!!).first() + + val conversationIdsToDelete = oldConversationsFromDb + .map { it.internalId } + .filterNot { it in conversationsFromSyncIds } + + dao.deleteConversations(conversationIdsToDelete) + } + + private suspend fun getListOfConversations(accountId: Long): List = + dao.getConversationsForUser(accountId).map { + it.map(ConversationEntity::asModel) + }.first() + + private suspend fun getConversation(accountId: Long, token: String): ConversationModel? { + val entity = dao.getConversationForUser(accountId, token).first() + return entity?.asModel() + } + + companion object { + val TAG = OfflineFirstConversationsRepository::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/RetrofitConversationsNetwork.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/RetrofitConversationsNetwork.kt new file mode 100644 index 0000000..9669688 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/network/RetrofitConversationsNetwork.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationlist.data.network + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.utils.ApiUtils +import io.reactivex.Observable + +class RetrofitConversationsNetwork(private val ncApi: NcApi) : ConversationsNetworkDataSource { + override fun getRooms(user: User, url: String, includeStatus: Boolean): Observable> { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) + + return ncApi.getRooms( + credentials, + ApiUtils.getUrlForRooms(apiVersion, user.baseUrl!!), + includeStatus + ).map { it -> + it.ocs?.data?.map { it } ?: listOf() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt new file mode 100644 index 0000000..f7ef8e2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -0,0 +1,228 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.conversationlist.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.invitation.data.InvitationsModel +import com.nextcloud.talk.invitation.data.InvitationsRepository +import com.nextcloud.talk.threadsoverview.data.ThreadsRepository +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.UserIdUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class ConversationsListViewModel @Inject constructor( + private val repository: OfflineConversationsRepository, + private val threadsRepository: ThreadsRepository, + private val currentUserProvider: CurrentUserProviderNew, + var userManager: UserManager +) : ViewModel() { + + @Inject + lateinit var invitationsRepository: InvitationsRepository + + @Inject + lateinit var arbitraryStorageManager: ArbitraryStorageManager + + private val _currentUser = currentUserProvider.currentUser.blockingGet() + val currentUser: User = _currentUser + val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: "" + + sealed interface ViewState + + sealed class ThreadsExistUiState { + data object None : ThreadsExistUiState() + data class Success(val threadsExistence: Boolean?) : ThreadsExistUiState() + data class Error(val exception: Exception) : ThreadsExistUiState() + } + + private val _threadsExistState = MutableStateFlow(ThreadsExistUiState.None) + val threadsExistState: StateFlow = _threadsExistState + + object GetRoomsStartState : ViewState + object GetRoomsErrorState : ViewState + open class GetRoomsSuccessState(val listIsNotEmpty: Boolean) : ViewState + + private val _getRoomsViewState: MutableLiveData = MutableLiveData(GetRoomsStartState) + val getRoomsViewState: LiveData + get() = _getRoomsViewState + + val getRoomsFlow = repository.roomListFlow + .onEach { list -> + _getRoomsViewState.value = GetRoomsSuccessState(list.isNotEmpty()) + }.catch { + _getRoomsViewState.value = GetRoomsErrorState + } + + object GetFederationInvitationsStartState : ViewState + object GetFederationInvitationsErrorState : ViewState + + open class GetFederationInvitationsSuccessState(val showInvitationsHint: Boolean) : ViewState + + private val _getFederationInvitationsViewState: MutableLiveData = + MutableLiveData(GetFederationInvitationsStartState) + val getFederationInvitationsViewState: LiveData + get() = _getFederationInvitationsViewState + + object ShowBadgeStartState : ViewState + object ShowBadgeErrorState : ViewState + open class ShowBadgeSuccessState(val showBadge: Boolean) : ViewState + + private val _showBadgeViewState: MutableLiveData = MutableLiveData(ShowBadgeStartState) + val showBadgeViewState: LiveData + get() = _showBadgeViewState + + fun getFederationInvitations() { + _getFederationInvitationsViewState.value = GetFederationInvitationsStartState + _showBadgeViewState.value = ShowBadgeStartState + + userManager.users.blockingGet()?.forEach { + invitationsRepository.fetchInvitations(it) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(FederatedInvitationsObserver()) + } + } + + fun getRooms() { + val startNanoTime = System.nanoTime() + Log.d(TAG, "fetchData - getRooms - calling: $startNanoTime") + repository.getRooms() + } + + fun checkIfThreadsExist() { + val limitForFollowedThreadsExistenceCheck = 1 + val accountId = UserIdUtils.getIdForUser(currentUserProvider.currentUser.blockingGet()) + + fun isLastCheckTooOld(lastCheckDate: Long): Boolean { + val currentTimeMillis = System.currentTimeMillis() + val differenceMillis = currentTimeMillis - lastCheckDate + val checkIntervalInMillies = TimeUnit.HOURS.toMillis(2) + return differenceMillis > checkIntervalInMillies + } + + fun checkIfFollowedThreadsExist() { + val threadsUrl = ApiUtils.getUrlForSubscribedThreads( + version = 1, + baseUrl = currentUser.baseUrl + ) + + viewModelScope.launch { + try { + val threads = + threadsRepository.getThreads(credentials, threadsUrl, limitForFollowedThreadsExistenceCheck) + val followedThreadsExistNew = threads.ocs?.data?.isNotEmpty() + _threadsExistState.value = ThreadsExistUiState.Success(followedThreadsExistNew) + val followedThreadsExistLastCheckNew = System.currentTimeMillis() + arbitraryStorageManager.storeStorageSetting( + accountId, + FOLLOWED_THREADS_EXIST_LAST_CHECK, + followedThreadsExistLastCheckNew.toString(), + "" + ) + arbitraryStorageManager.storeStorageSetting( + accountId, + FOLLOWED_THREADS_EXIST, + followedThreadsExistNew.toString(), + "" + ) + } catch (exception: Exception) { + _threadsExistState.value = ThreadsExistUiState.Error(exception) + } + } + } + + if (!hasSpreedFeatureCapability(currentUser.capabilities!!.spreedCapability!!, SpreedFeatures.THREADS)) { + _threadsExistState.value = ThreadsExistUiState.Success(false) + return + } + + val followedThreadsExistOld = arbitraryStorageManager.getStorageSetting( + accountId, + FOLLOWED_THREADS_EXIST, + "" + ).blockingGet()?.value?.toBoolean() ?: false + + val followedThreadsExistLastCheckOld = arbitraryStorageManager.getStorageSetting( + accountId, + FOLLOWED_THREADS_EXIST_LAST_CHECK, + "" + ).blockingGet()?.value?.toLong() + + if (followedThreadsExistOld) { + Log.d(TAG, "followed threads exist for this user. No need to check again.") + _threadsExistState.value = ThreadsExistUiState.Success(true) + } else { + if (followedThreadsExistLastCheckOld == null || isLastCheckTooOld(followedThreadsExistLastCheckOld)) { + Log.d(TAG, "check if followed threads exist never happened or is too old. Checking now...") + checkIfFollowedThreadsExist() + } else { + _threadsExistState.value = ThreadsExistUiState.Success(false) + Log.d(TAG, "already checked in the last 2 hours if followed threads exist. Skip check.") + } + } + } + + inner class FederatedInvitationsObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(invitationsModel: InvitationsModel) { + val currentUser = currentUserProvider.currentUser.blockingGet() + + if (invitationsModel.user.userId?.equals(currentUser.userId) == true && + invitationsModel.user.baseUrl?.equals(currentUser.baseUrl) == true + ) { + if (invitationsModel.invitations.isNotEmpty()) { + _getFederationInvitationsViewState.value = GetFederationInvitationsSuccessState(true) + } else { + _getFederationInvitationsViewState.value = GetFederationInvitationsSuccessState(false) + } + } else { + if (invitationsModel.invitations.isNotEmpty()) { + _showBadgeViewState.value = ShowBadgeSuccessState(true) + } + } + } + + override fun onError(e: Throwable) { + _getFederationInvitationsViewState.value = GetFederationInvitationsErrorState + Log.e(TAG, "Failed to fetch pending invitations", e) + } + + override fun onComplete() { + // unused atm + } + } + + companion object { + private val TAG = ConversationsListViewModel::class.simpleName + const val FOLLOWED_THREADS_EXIST_LAST_CHECK = "FOLLOWED_THREADS_EXIST_LAST_CHECK" + const val FOLLOWED_THREADS_EXIST = "FOLLOWED_THREADS_EXIST" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/BusModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/BusModule.java new file mode 100644 index 0000000..f3ee3e0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/BusModule.java @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.dagger.modules; + +import dagger.Module; +import dagger.Provides; + +import org.greenrobot.eventbus.EventBus; + +import javax.inject.Singleton; + +@Module +public class BusModule { + + @Provides + @Singleton + public EventBus provideEventBus() { + return EventBus.getDefault(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ContextModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/ContextModule.java new file mode 100644 index 0000000..462bdb4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ContextModule.java @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.dagger.modules; + +import android.content.Context; + +import androidx.annotation.NonNull; +import dagger.Module; +import dagger.Provides; + +@Module +public class ContextModule { + private final Context context; + + public ContextModule(@NonNull final Context context) { + this.context = context; + } + + @Provides + public Context provideContext() { + return context; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/DaosModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/DaosModule.kt new file mode 100644 index 0000000..27ec540 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/DaosModule.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.dagger.modules + +import com.nextcloud.talk.data.database.dao.ChatBlocksDao +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.database.dao.ConversationsDao +import com.nextcloud.talk.data.source.local.TalkDatabase +import dagger.Module +import dagger.Provides + +@Module +internal object DaosModule { + @Provides + fun providesConversationsDao(database: TalkDatabase): ConversationsDao = database.conversationsDao() + + @Provides + fun providesChatDao(database: TalkDatabase): ChatMessagesDao = database.chatMessagesDao() + + @Provides + fun providesChatBlocksDao(database: TalkDatabase): ChatBlocksDao = database.chatBlocksDao() +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java new file mode 100644 index 0000000..c7b08a6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java @@ -0,0 +1,54 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.dagger.modules; + +import android.content.Context; + +import com.nextcloud.talk.data.network.NetworkMonitor; +import com.nextcloud.talk.data.network.NetworkMonitorImpl; +import com.nextcloud.talk.data.source.local.TalkDatabase; +import com.nextcloud.talk.utils.preferences.AppPreferences; +import com.nextcloud.talk.utils.preferences.AppPreferencesImpl; + +import javax.inject.Singleton; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import dagger.Module; +import dagger.Provides; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + +@Module +@OptIn(markerClass = ExperimentalCoroutinesApi.class) +public class DatabaseModule { + @Provides + @Singleton + public AppPreferences providePreferences(@NonNull final Context poContext) { + AppPreferences preferences = new AppPreferencesImpl(poContext); + preferences.removeLinkPreviews(); + return preferences; + } + + @Provides + @Singleton + public AppPreferencesImpl providePreferencesImpl(@NonNull final Context poContext) { + return new AppPreferencesImpl(poContext); + } + + @Provides + @Singleton + public TalkDatabase provideTalkDatabase(@NonNull final Context context) { + return TalkDatabase.getInstance(context); + } + + @Provides + @Singleton + public NetworkMonitor provideNetworkMonitor(@NonNull final Context poContext) { + return new NetworkMonitorImpl(poContext); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ManagerModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ManagerModule.kt new file mode 100644 index 0000000..cc7b14f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ManagerModule.kt @@ -0,0 +1,36 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.dagger.modules + +import android.content.Context +import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager +import com.nextcloud.talk.chat.data.io.AudioRecorderManager +import com.nextcloud.talk.chat.data.io.MediaPlayerManager +import com.nextcloud.talk.chat.data.io.MediaRecorderManager +import com.nextcloud.talk.utils.preferences.AppPreferences +import dagger.Module +import dagger.Provides + +@Module +class ManagerModule { + + @Provides + fun provideMediaRecorderManager(): MediaRecorderManager = MediaRecorderManager() + + @Provides + fun provideAudioRecorderManager(): AudioRecorderManager = AudioRecorderManager() + + @Provides + fun provideMediaPlayerManager(preferences: AppPreferences): MediaPlayerManager = + MediaPlayerManager().apply { + appPreferences = preferences + } + + @Provides + fun provideAudioFocusManager(context: Context): AudioFocusRequestManager = AudioFocusRequestManager(context) +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt new file mode 100644 index 0000000..2434cf6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -0,0 +1,218 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022-2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.dagger.modules + +import android.content.Context +import com.nextcloud.talk.account.data.LoginRepository +import com.nextcloud.talk.account.data.io.LocalLoginDataSource +import com.nextcloud.talk.account.data.network.NetworkLoginDataSource +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.chat.data.ChatMessageRepository +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository +import com.nextcloud.talk.chat.data.network.RetrofitChatNetwork +import com.nextcloud.talk.contacts.ContactsRepository +import com.nextcloud.talk.contacts.ContactsRepositoryImpl +import com.nextcloud.talk.conversationcreation.ConversationCreationRepository +import com.nextcloud.talk.conversationcreation.ConversationCreationRepositoryImpl +import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository +import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepositoryImpl +import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository +import com.nextcloud.talk.conversationlist.data.network.ConversationsNetworkDataSource +import com.nextcloud.talk.conversationlist.data.network.OfflineFirstConversationsRepository +import com.nextcloud.talk.conversationlist.data.network.RetrofitConversationsNetwork +import com.nextcloud.talk.data.database.dao.ChatBlocksDao +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.database.dao.ConversationsDao +import com.nextcloud.talk.data.network.NetworkMonitor +import com.nextcloud.talk.data.source.local.TalkDatabase +import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository +import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl +import com.nextcloud.talk.data.user.UsersRepository +import com.nextcloud.talk.data.user.UsersRepositoryImpl +import com.nextcloud.talk.invitation.data.InvitationsRepository +import com.nextcloud.talk.invitation.data.InvitationsRepositoryImpl +import com.nextcloud.talk.openconversations.data.OpenConversationsRepository +import com.nextcloud.talk.openconversations.data.OpenConversationsRepositoryImpl +import com.nextcloud.talk.polls.repositories.PollRepository +import com.nextcloud.talk.polls.repositories.PollRepositoryImpl +import com.nextcloud.talk.raisehand.RequestAssistanceRepository +import com.nextcloud.talk.raisehand.RequestAssistanceRepositoryImpl +import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepository +import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepositoryImpl +import com.nextcloud.talk.repositories.callrecording.CallRecordingRepository +import com.nextcloud.talk.repositories.callrecording.CallRecordingRepositoryImpl +import com.nextcloud.talk.repositories.conversations.ConversationsRepository +import com.nextcloud.talk.repositories.conversations.ConversationsRepositoryImpl +import com.nextcloud.talk.repositories.reactions.ReactionsRepository +import com.nextcloud.talk.repositories.reactions.ReactionsRepositoryImpl +import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository +import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepositoryImpl +import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository +import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl +import com.nextcloud.talk.threadsoverview.data.ThreadsRepository +import com.nextcloud.talk.threadsoverview.data.ThreadsRepositoryImpl +import com.nextcloud.talk.translate.repositories.TranslateRepository +import com.nextcloud.talk.translate.repositories.TranslateRepositoryImpl +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.preferences.AppPreferences +import dagger.Module +import dagger.Provides +import okhttp3.OkHttpClient + +@Module +class RepositoryModule { + + @Provides + fun provideConversationsRepository( + ncApi: NcApi, + ncApiCoroutines: NcApiCoroutines, + userProvider: CurrentUserProviderNew + ): ConversationsRepository = ConversationsRepositoryImpl(ncApi, ncApiCoroutines, userProvider) + + @Provides + fun provideSharedItemsRepository(ncApi: NcApi, dateUtils: DateUtils): SharedItemsRepository = + SharedItemsRepositoryImpl(ncApi, dateUtils) + + @Provides + fun provideUnifiedSearchRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): UnifiedSearchRepository = + UnifiedSearchRepositoryImpl(ncApi, userProvider) + + @Provides + fun provideDialogPollRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): PollRepository = + PollRepositoryImpl(ncApi, userProvider) + + @Provides + fun provideRemoteFileBrowserItemsRepository( + okHttpClient: OkHttpClient, + userProvider: CurrentUserProviderNew + ): RemoteFileBrowserItemsRepository = RemoteFileBrowserItemsRepositoryImpl(okHttpClient, userProvider) + + @Provides + fun provideUsersRepository(database: TalkDatabase): UsersRepository = UsersRepositoryImpl(database.usersDao()) + + @Provides + fun provideArbitraryStoragesRepository(database: TalkDatabase): ArbitraryStoragesRepository = + ArbitraryStoragesRepositoryImpl(database.arbitraryStoragesDao()) + + @Provides + fun provideReactionsRepository( + ncApi: NcApi, + userProvider: CurrentUserProviderNew, + dao: ChatMessagesDao + ): ReactionsRepository = ReactionsRepositoryImpl(ncApi, userProvider, dao) + + @Provides + fun provideCallRecordingRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): CallRecordingRepository = + CallRecordingRepositoryImpl(ncApi, userProvider) + + @Provides + fun provideRequestAssistanceRepository( + ncApi: NcApi, + userProvider: CurrentUserProviderNew + ): RequestAssistanceRepository = RequestAssistanceRepositoryImpl(ncApi, userProvider) + + @Provides + fun provideOpenConversationsRepository( + ncApi: NcApi, + userProvider: CurrentUserProviderNew + ): OpenConversationsRepository = OpenConversationsRepositoryImpl(ncApi, userProvider) + + @Provides + fun translateRepository(ncApi: NcApi): TranslateRepository = TranslateRepositoryImpl(ncApi) + + @Provides + fun provideChatNetworkDataSource(ncApi: NcApi, ncApiCoroutines: NcApiCoroutines): ChatNetworkDataSource = + RetrofitChatNetwork(ncApi, ncApiCoroutines) + + @Provides + fun provideConversationsNetworkDataSource(ncApi: NcApi): ConversationsNetworkDataSource = + RetrofitConversationsNetwork(ncApi) + + @Provides + fun provideConversationInfoEditRepository( + ncApi: NcApi, + ncApiCoroutines: NcApiCoroutines, + userProvider: CurrentUserProviderNew + ): ConversationInfoEditRepository = ConversationInfoEditRepositoryImpl(ncApi, ncApiCoroutines, userProvider) + + @Provides + fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository = InvitationsRepositoryImpl(ncApi) + + @Provides + fun provideOfflineFirstChatRepository( + chatMessagesDao: ChatMessagesDao, + chatBlocksDao: ChatBlocksDao, + dataSource: ChatNetworkDataSource, + networkMonitor: NetworkMonitor, + userProvider: CurrentUserProviderNew + ): ChatMessageRepository = + OfflineFirstChatRepository( + chatMessagesDao, + chatBlocksDao, + dataSource, + networkMonitor, + userProvider + ) + + @Provides + fun provideOfflineFirstConversationsRepository( + dao: ConversationsDao, + dataSource: ConversationsNetworkDataSource, + chatNetworkDataSource: ChatNetworkDataSource, + networkMonitor: NetworkMonitor, + currentUserProviderNew: CurrentUserProviderNew + ): OfflineConversationsRepository = + OfflineFirstConversationsRepository( + dao, + dataSource, + chatNetworkDataSource, + networkMonitor, + currentUserProviderNew + ) + + @Provides + fun provideContactsRepository( + ncApiCoroutines: NcApiCoroutines, + currentUserProviderNew: CurrentUserProviderNew + ): ContactsRepository = ContactsRepositoryImpl(ncApiCoroutines, currentUserProviderNew) + + @Provides + fun provideConversationCreationRepository( + ncApiCoroutines: NcApiCoroutines, + currentUserProviderNew: CurrentUserProviderNew + ): ConversationCreationRepository = ConversationCreationRepositoryImpl(ncApiCoroutines, currentUserProviderNew) + + @Provides + fun provideThreadsRepository( + ncApiCoroutines: NcApiCoroutines, + currentUserProviderNew: CurrentUserProviderNew + ): ThreadsRepository = ThreadsRepositoryImpl(ncApiCoroutines, currentUserProviderNew) + + @Provides + fun provideNetworkDataSource(okHttpClient: OkHttpClient): NetworkLoginDataSource = + NetworkLoginDataSource(okHttpClient) + + @Provides + fun providesLocalDataSource( + userManager: UserManager, + appPreferences: AppPreferences, + context: Context + ): LocalLoginDataSource = LocalLoginDataSource(userManager, appPreferences, context) + + @Provides + fun provideLoginRepository( + networkLoginDataSource: NetworkLoginDataSource, + localLoginDataSource: LocalLoginDataSource + ): LoginRepository = LoginRepository(networkLoginDataSource, localLoginDataSource) +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java new file mode 100644 index 0000000..8d8233a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java @@ -0,0 +1,328 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.dagger.modules; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import com.github.aurae.retrofit2.LoganSquareConverterFactory; +import com.nextcloud.talk.BuildConfig; +import com.nextcloud.talk.R; +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.api.NcApiCoroutines; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.users.UserManager; +import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.LoggingUtils; +import com.nextcloud.talk.utils.preferences.AppPreferences; +import com.nextcloud.talk.utils.ssl.KeyManager; +import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat; +import com.nextcloud.talk.utils.ssl.TrustManager; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.inject.Singleton; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.X509KeyManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import dagger.Module; +import dagger.Provides; +import io.reactivex.schedulers.Schedulers; +import okhttp3.Authenticator; +import okhttp3.Cache; +import okhttp3.ConnectionSpec; +import okhttp3.Credentials; +import okhttp3.Dispatcher; +import okhttp3.Interceptor; +import okhttp3.JavaNetCookieJar; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.Route; +import okhttp3.internal.tls.OkHostnameVerifier; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; + +@Module(includes = DatabaseModule.class) +public class RestModule { + + private static final String TAG = "RestModule"; + private final Context context; + + public RestModule(Context context) { + this.context = context; + } + + @Singleton + @Provides + NcApi provideNcApi(Retrofit retrofit) { + return retrofit.create(NcApi.class); + } + + @Singleton + @Provides + NcApiCoroutines provideNcApiCoroutines(Retrofit retrofit) { + return retrofit.create(NcApiCoroutines.class); + } + + + @Singleton + @Provides + Proxy provideProxy(AppPreferences appPreferences) { + if (!TextUtils.isEmpty(appPreferences.getProxyType()) && !"No proxy".equals(appPreferences.getProxyType()) + && !TextUtils.isEmpty(appPreferences.getProxyHost())) { + GetProxyRunnable getProxyRunnable = new GetProxyRunnable(appPreferences); + Thread getProxyThread = new Thread(getProxyRunnable); + getProxyThread.start(); + try { + getProxyThread.join(); + return getProxyRunnable.getProxyValue(); + } catch (InterruptedException e) { + Log.e(TAG, "Failed to join the thread while getting proxy: " + e.getLocalizedMessage()); + return Proxy.NO_PROXY; + } + } else { + return Proxy.NO_PROXY; + } + } + + @Singleton + @Provides + Retrofit provideRetrofit(OkHttpClient httpClient) { + Retrofit.Builder retrofitBuilder = new Retrofit.Builder() + .client(httpClient) + .baseUrl("https://nextcloud.com") + .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) + .addConverterFactory(LoganSquareConverterFactory.create()); + + return retrofitBuilder.build(); + } + + @Singleton + @Provides + TrustManager provideTrustManager() { + return new TrustManager(); + } + + @Singleton + @Provides + KeyManager provideKeyManager(AppPreferences appPreferences, UserManager userManager) { + KeyStore keyStore = null; + try { + keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, null); + X509KeyManager origKm = (X509KeyManager) kmf.getKeyManagers()[0]; + return new KeyManager(origKm, userManager, appPreferences); + } catch (KeyStoreException e) { + Log.e(TAG, "KeyStoreException " + e.getLocalizedMessage()); + } catch (CertificateException e) { + Log.e(TAG, "CertificateException " + e.getLocalizedMessage()); + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "NoSuchAlgorithmException " + e.getLocalizedMessage()); + } catch (IOException e) { + Log.e(TAG, "IOException " + e.getLocalizedMessage()); + } catch (UnrecoverableKeyException e) { + Log.e(TAG, "UnrecoverableKeyException " + e.getLocalizedMessage()); + } + + return null; + } + + @Singleton + @Provides + SSLSocketFactoryCompat provideSslSocketFactoryCompat(KeyManager keyManager, TrustManager + trustManager) { + return new SSLSocketFactoryCompat(keyManager, trustManager); + } + + @Singleton + @Provides + CookieManager provideCookieManager() { + return new CookieManager(); + } + + @Singleton + @Provides + Cache provideCache() { + int cacheSize = 128 * 1024 * 1024; // 128 MB + + return new Cache(NextcloudTalkApplication.Companion.getSharedApplication().getCacheDir(), cacheSize); + } + + @Singleton + @Provides + Dispatcher provideDispatcher() { + Dispatcher dispatcher = new Dispatcher(); + dispatcher.setMaxRequestsPerHost(100); + dispatcher.setMaxRequests(100); + return dispatcher; + } + + @Singleton + @Provides + OkHttpClient provideHttpClient(Proxy proxy, AppPreferences appPreferences, + TrustManager trustManager, + SSLSocketFactoryCompat sslSocketFactoryCompat, Cache cache, + CookieManager cookieManager, Dispatcher dispatcher) { + OkHttpClient.Builder httpClient = new OkHttpClient.Builder(); + + httpClient.retryOnConnectionFailure(true); + httpClient.connectTimeout(45, TimeUnit.SECONDS); + httpClient.readTimeout(45, TimeUnit.SECONDS); + httpClient.writeTimeout(45, TimeUnit.SECONDS); + + httpClient.cookieJar(new JavaNetCookieJar(cookieManager)); + httpClient.cache(cache); + + // Trust own CA and all self-signed certs + httpClient.sslSocketFactory(sslSocketFactoryCompat, trustManager); + httpClient.retryOnConnectionFailure(true); + httpClient.hostnameVerifier(trustManager.getHostnameVerifier(OkHostnameVerifier.INSTANCE)); + + httpClient.dispatcher(dispatcher); + if (!Proxy.NO_PROXY.equals(proxy)) { + httpClient.proxy(proxy); + + if (appPreferences.getProxyCredentials() && + !TextUtils.isEmpty(appPreferences.getProxyUsername()) && + !TextUtils.isEmpty(appPreferences.getProxyPassword())) { + httpClient.proxyAuthenticator(new HttpAuthenticator( + Credentials.basic( + appPreferences.getProxyUsername(), + appPreferences.getProxyPassword(), + StandardCharsets.UTF_8), + "Proxy-Authorization")); + } + } + + httpClient.addInterceptor(new HeadersInterceptor()); + + List specs = new ArrayList<>(); + if (BuildConfig.DEBUG) { + specs.add(ConnectionSpec.COMPATIBLE_TLS); + specs.add(ConnectionSpec.CLEARTEXT); + httpClient.connectionSpecs(specs); + } else { + specs.add(ConnectionSpec.COMPATIBLE_TLS); + httpClient.connectionSpecs(specs); + } + + if (BuildConfig.DEBUG && !context.getResources().getBoolean(R.bool.nc_is_debug)) { + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + loggingInterceptor.redactHeader("Authorization"); + loggingInterceptor.redactHeader("Proxy-Authorization"); + httpClient.addInterceptor(loggingInterceptor); + } else if (context.getResources().getBoolean(R.bool.nc_is_debug)) { + HttpLoggingInterceptor.Logger fileLogger = + s -> LoggingUtils.INSTANCE.writeLogEntryToFile(context, s); + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(fileLogger); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + loggingInterceptor.redactHeader("Authorization"); + loggingInterceptor.redactHeader("Proxy-Authorization"); + httpClient.addInterceptor(loggingInterceptor); + } + + return httpClient.build(); + } + + public static class HeadersInterceptor implements Interceptor { + + @NonNull + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + Request original = chain.request(); + Request request = original.newBuilder() + .header("User-Agent", ApiUtils.getUserAgent()) + .header("Accept", "application/json") + .header("OCS-APIRequest", "true") + .header("ngrok-skip-browser-warning", "true") + .method(original.method(), original.body()) + .build(); + + return chain.proceed(request); + } + } + + public static class HttpAuthenticator implements Authenticator { + + private final String credentials; + private final String authenticatorType; + + public HttpAuthenticator(@NonNull String credentials, @NonNull String authenticatorType) { + this.credentials = credentials; + this.authenticatorType = authenticatorType; + } + + @Nullable + @Override + public Request authenticate(@Nullable Route route, @NonNull Response response) { + if (response.request().header(authenticatorType) != null) { + return null; + } + + Response countedResponse = response; + + int attemptsCount = 0; + + while ((countedResponse = countedResponse.priorResponse()) != null) { + attemptsCount++; + if (attemptsCount == 3) { + return null; + } + } + + return response.request().newBuilder() + .header(authenticatorType, credentials) + .build(); + } + } + + private class GetProxyRunnable implements Runnable { + private volatile Proxy proxy; + private final AppPreferences appPreferences; + + GetProxyRunnable(AppPreferences appPreferences) { + this.appPreferences = appPreferences; + } + + @Override + public void run() { + if (Proxy.Type.valueOf(appPreferences.getProxyType()) == Proxy.Type.SOCKS) { + proxy = new Proxy(Proxy.Type.valueOf(appPreferences.getProxyType()), + InetSocketAddress.createUnresolved(appPreferences.getProxyHost(), Integer.parseInt( + appPreferences.getProxyPort()))); + } else { + proxy = new Proxy(Proxy.Type.valueOf(appPreferences.getProxyType()), + new InetSocketAddress(appPreferences.getProxyHost(), + Integer.parseInt(appPreferences.getProxyPort()))); + } + } + + Proxy getProxyValue() { + return proxy; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/UtilsModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/UtilsModule.kt new file mode 100644 index 0000000..91861a3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/UtilsModule.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.dagger.modules + +import android.content.Context +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtilImpl +import dagger.Module +import dagger.Provides +import dagger.Reusable + +@Module(includes = [ContextModule::class]) +class UtilsModule { + @Provides + @Reusable + fun providePermissionUtil(context: Context): PlatformPermissionUtil = PlatformPermissionUtilImpl(context) + + @Provides + @Reusable + fun provideDateUtils(context: Context): DateUtils = DateUtils(context) + + @Provides + @Reusable + fun provideMessageUtils(context: Context): MessageUtils = MessageUtils(context) +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt new file mode 100644 index 0000000..7d44652 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -0,0 +1,169 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.dagger.modules + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.nextcloud.talk.account.viewmodels.BrowserLoginActivityViewModel +import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel +import com.nextcloud.talk.contacts.ContactsViewModel +import com.nextcloud.talk.conversationcreation.ConversationCreationViewModel +import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel +import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel +import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel +import com.nextcloud.talk.diagnose.DiagnoseViewModel +import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel +import com.nextcloud.talk.messagesearch.MessageSearchViewModel +import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel +import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel +import com.nextcloud.talk.polls.viewmodels.PollMainViewModel +import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel +import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel +import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel +import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel +import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel +import com.nextcloud.talk.threadsoverview.viewmodels.ThreadsOverviewViewModel +import com.nextcloud.talk.translate.viewmodels.TranslateViewModel +import com.nextcloud.talk.viewmodels.CallRecordingViewModel +import dagger.Binds +import dagger.MapKey +import dagger.Module +import dagger.multibindings.IntoMap +import javax.inject.Inject +import javax.inject.Provider +import kotlin.reflect.KClass + +class ViewModelFactory @Inject constructor( + private val viewModels: MutableMap, Provider> +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T = viewModels[modelClass]?.get() as T +} + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@Retention(AnnotationRetention.RUNTIME) +@MapKey +internal annotation class ViewModelKey(val value: KClass) + +@Module +@Suppress("TooManyFunctions") +abstract class ViewModelModule { + + @Binds + abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory + + @Binds + @IntoMap + @ViewModelKey(SharedItemsViewModel::class) + abstract fun sharedItemsViewModel(viewModel: SharedItemsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(MessageSearchViewModel::class) + abstract fun messageSearchViewModel(viewModel: MessageSearchViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(PollMainViewModel::class) + abstract fun pollViewModel(viewModel: PollMainViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(PollVoteViewModel::class) + abstract fun pollVoteViewModel(viewModel: PollVoteViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(PollResultsViewModel::class) + abstract fun pollResultsViewModel(viewModel: PollResultsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(PollCreateViewModel::class) + abstract fun pollCreateViewModel(viewModel: PollCreateViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RemoteFileBrowserItemsViewModel::class) + abstract fun remoteFileBrowserItemsViewModel(viewModel: RemoteFileBrowserItemsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(CallRecordingViewModel::class) + abstract fun callRecordingViewModel(viewModel: CallRecordingViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RaiseHandViewModel::class) + abstract fun raiseHandViewModel(viewModel: RaiseHandViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(TranslateViewModel::class) + abstract fun translateViewModel(viewModel: TranslateViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(OpenConversationsViewModel::class) + abstract fun openConversationsViewModel(viewModel: OpenConversationsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ConversationsListViewModel::class) + abstract fun conversationsListViewModel(viewModel: ConversationsListViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ChatViewModel::class) + abstract fun chatViewModel(viewModel: ChatViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(MessageInputViewModel::class) + abstract fun messageInputViewModel(viewModel: MessageInputViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ConversationInfoViewModel::class) + abstract fun conversationInfoViewModel(viewModel: ConversationInfoViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ConversationInfoEditViewModel::class) + abstract fun conversationInfoEditViewModel(viewModel: ConversationInfoEditViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(InvitationsViewModel::class) + abstract fun invitationsViewModel(viewModel: InvitationsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ContactsViewModel::class) + abstract fun contactsViewModel(viewModel: ContactsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ConversationCreationViewModel::class) + abstract fun conversationCreationViewModel(viewModel: ConversationCreationViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(DiagnoseViewModel::class) + abstract fun diagnoseViewModel(viewModel: DiagnoseViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ThreadsOverviewViewModel::class) + abstract fun threadsOverviewViewModel(viewModel: ThreadsOverviewViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(BrowserLoginActivityViewModel::class) + abstract fun browserLoginActivityViewModel(viewModel: BrowserLoginActivityViewModel): ViewModel +} diff --git a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt new file mode 100644 index 0000000..7c80cb8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatBlocksDao.kt @@ -0,0 +1,84 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.nextcloud.talk.data.database.model.ChatBlockEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ChatBlocksDao { + @Delete + fun deleteChatBlocks(blocks: List) + + @Query( + """ + SELECT * + FROM ChatBlocks + WHERE internalConversationId in (:internalConversationId) + AND (threadId = :threadId OR (threadId IS NULL AND :threadId IS NULL)) + AND oldestMessageId <= :messageId + AND newestMessageId >= :messageId + ORDER BY newestMessageId ASC + """ + ) + fun getChatBlocksContainingMessageId( + internalConversationId: String, + threadId: Long?, + messageId: Long + ): Flow> + + @Query( + """ + SELECT * + FROM ChatBlocks + WHERE internalConversationId = :internalConversationId + AND (threadId = :threadId OR (threadId IS NULL AND :threadId IS NULL)) + AND( + (oldestMessageId <= :oldestMessageId AND newestMessageId >= :oldestMessageId) + OR + (oldestMessageId <= :newestMessageId AND newestMessageId >= :newestMessageId) + OR + (oldestMessageId >= :oldestMessageId AND newestMessageId <= :newestMessageId) + ) + ORDER BY newestMessageId ASC + """ + ) + fun getConnectedChatBlocks( + internalConversationId: String, + threadId: Long?, + oldestMessageId: Long, + newestMessageId: Long + ): Flow> + + @Query( + """ + SELECT MAX(newestMessageId) as max_items + FROM ChatBlocks + WHERE internalConversationId = :internalConversationId + AND (threadId = :threadId OR (threadId IS NULL AND :threadId IS NULL)) + """ + ) + fun getNewestMessageIdFromChatBlocks(internalConversationId: String, threadId: Long?): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertChatBlock(chatBlock: ChatBlockEntity) + + @Query( + """ + DELETE FROM ChatBlocks + WHERE internalConversationId = :internalConversationId + AND oldestMessageId < :messageId + """ + ) + fun deleteChatBlocksOlderThan(internalConversationId: String, messageId: Long) +} diff --git a/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt new file mode 100644 index 0000000..b3c5276 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ChatMessagesDao.kt @@ -0,0 +1,224 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import kotlinx.coroutines.flow.Flow + +@Dao +@Suppress("Detekt.TooManyFunctions") +interface ChatMessagesDao { + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND isTemporary = 0 + ORDER BY timestamp DESC, id DESC + """ + ) + fun getMessagesForConversation(internalConversationId: String): Flow> + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND isTemporary = 1 + ORDER BY timestamp DESC, id DESC + """ + ) + fun getTempMessagesForConversation(internalConversationId: String): Flow> + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND isTemporary = 1 + AND sendStatus != 'SENT_PENDING_ACK' + AND (:threadId IS NULL OR threadId = :threadId) + ORDER BY timestamp DESC, id DESC + """ + ) + fun getTempUnsentMessagesForConversation( + internalConversationId: String, + threadId: Long? + ): Flow> + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND referenceId = :referenceId + AND isTemporary = 1 + AND (:threadId IS NULL OR threadId = :threadId) + ORDER BY timestamp DESC, id DESC + """ + ) + fun getTempMessageForConversation( + internalConversationId: String, + referenceId: String, + threadId: Long? + ): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertChatMessages(chatMessages: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertChatMessage(chatMessage: ChatMessageEntity) + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND id = :messageId + """ + ) + fun getChatMessageForConversation(internalConversationId: String, messageId: Long): Flow + + @Query( + value = """ + DELETE FROM ChatMessages + WHERE internalId in (:internalIds) + """ + ) + fun deleteChatMessages(internalIds: List) + + @Query( + value = """ + DELETE FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND referenceId in (:referenceIds) + AND isTemporary = 1 + """ + ) + fun deleteTempChatMessages(internalConversationId: String, referenceIds: List) + + @Update + fun updateChatMessage(message: ChatMessageEntity) + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE id in (:messageIds) + ORDER BY timestamp ASC, id ASC + """ + ) + fun getMessagesFromIds(messageIds: List): Flow> + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId AND id >= :messageId + AND isTemporary = 0 + AND (:threadId IS NULL OR threadId = :threadId) + ORDER BY timestamp ASC, id ASC + """ + ) + fun getMessagesForConversationSince( + internalConversationId: String, + messageId: Long, + threadId: Long? + ): Flow> + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND isTemporary = 0 + AND id < :messageId + AND (:threadId IS NULL OR threadId = :threadId) + ORDER BY timestamp DESC, id DESC + LIMIT :limit + """ + ) + fun getMessagesForConversationBefore( + internalConversationId: String, + messageId: Long, + limit: Int, + threadId: Long? + ): Flow> + + @Query( + """ + SELECT * + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND isTemporary = 0 + AND id <= :messageId + AND (:threadId IS NULL OR threadId = :threadId) + ORDER BY timestamp DESC, id DESC + LIMIT :limit + """ + ) + fun getMessagesForConversationBeforeAndEqual( + internalConversationId: String, + messageId: Long, + limit: Int, + threadId: Long? + ): Flow> + + @Query( + """ + SELECT COUNT(*) + FROM ChatMessages + WHERE internalConversationId = :internalConversationId + AND isTemporary = 0 + AND (:threadId IS NULL OR threadId = :threadId) + AND id BETWEEN :newestMessageId AND :oldestMessageId + """ + ) + fun getCountBetweenMessageIds( + internalConversationId: String, + oldestMessageId: Long, + newestMessageId: Long, + threadId: Long? + ): Int + + @Query( + """ + DELETE FROM chatmessages + WHERE internalId LIKE :pattern + """ + ) + fun clearAllMessagesForUser(pattern: String) + + @Query( + """ + DELETE FROM chatmessages + WHERE internalConversationId = :internalConversationId + AND id < :messageId + """ + ) + fun deleteMessagesOlderThan(internalConversationId: String, messageId: Long) + + @Query( + """ + SELECT COUNT(*) + FROM ChatMessages AS child + INNER JOIN ChatMessages AS parent + ON child.parent = parent.id + WHERE child.internalConversationId = :internalConversationId + AND child.isTemporary = 0 + AND child.messageType = 'comment' + AND parent.threadId = :threadId + """ + ) + fun getNumberOfThreadReplies(internalConversationId: String, threadId: Long): Int +} diff --git a/app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt b/app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt new file mode 100644 index 0000000..621207c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/dao/ConversationsDao.kt @@ -0,0 +1,66 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy.Companion.REPLACE +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import com.nextcloud.talk.data.database.model.ConversationEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +@Dao +interface ConversationsDao { + @Query("SELECT * FROM Conversations where accountId = :accountId") + fun getConversationsForUser(accountId: Long): Flow> + + @Query("SELECT * FROM Conversations where accountId = :accountId AND token = :token") + fun getConversationForUser(accountId: Long, token: String): Flow + + @Transaction + suspend fun upsertConversations(accountId: Long, serverItems: List) { + serverItems.forEach { serverItem -> + val existingItem = getConversationForUser(accountId, serverItem.token).first() + if (existingItem != null) { + val mergedItem = serverItem.copy() + mergedItem.messageDraft = existingItem.messageDraft + updateConversation(mergedItem) + } else { + insertConversation(serverItem) + } + } + } + + /** + * Deletes rows in the db matching the specified [conversationIds] + */ + @Query( + value = """ + DELETE FROM conversations + WHERE internalId in (:conversationIds) + """ + ) + fun deleteConversations(conversationIds: List) + + @Update(onConflict = REPLACE) + fun updateConversation(conversationEntity: ConversationEntity) + + @Insert(onConflict = REPLACE) + fun insertConversation(conversation: ConversationEntity) + + @Query( + """ + DELETE FROM Conversations + WHERE accountId = :accountId + """ + ) + fun clearAllConversationsForUser(accountId: Long) +} diff --git a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt new file mode 100644 index 0000000..1369740 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ChatMessageMapUtils.kt @@ -0,0 +1,113 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.mappers + +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.models.json.chat.ReadStatus + +fun ChatMessageJson.asEntity(accountId: Long) = + ChatMessageEntity( + // accountId@token@messageId + internalId = "$accountId@$token@$id", + accountId = accountId, + id = id, + internalConversationId = "$accountId@$token", + threadId = threadId, + isThread = hasThread, + message = message!!, + token = token!!, + actorType = actorType!!, + actorId = actorId!!, + actorDisplayName = actorDisplayName!!, + timestamp = timestamp, + messageParameters = messageParameters, + systemMessageType = systemMessageType!!, + replyable = replyable, + parentMessageId = parentMessage?.id, + messageType = messageType!!, + reactions = reactions, + reactionsSelf = reactionsSelf, + expirationTimestamp = expirationTimestamp, + renderMarkdown = renderMarkdown, + lastEditActorDisplayName = lastEditActorDisplayName, + lastEditActorId = lastEditActorId, + lastEditActorType = lastEditActorType, + lastEditTimestamp = lastEditTimestamp, + deleted = deleted, + referenceId = referenceId, + silent = silent, + threadTitle = threadTitle, + threadReplies = threadReplies + ) + +fun ChatMessageEntity.asModel() = + ChatMessage( + jsonMessageId = id.toInt(), + message = message, + token = token, + threadId = threadId, + isThread = isThread, + actorType = actorType, + actorId = actorId, + actorDisplayName = actorDisplayName, + timestamp = timestamp, + messageParameters = messageParameters, + systemMessageType = systemMessageType, + replyable = replyable, + parentMessageId = parentMessageId, + messageType = messageType, + reactions = reactions, + reactionsSelf = reactionsSelf, + expirationTimestamp = expirationTimestamp, + renderMarkdown = renderMarkdown, + lastEditActorDisplayName = lastEditActorDisplayName, + lastEditActorId = lastEditActorId, + lastEditActorType = lastEditActorType, + lastEditTimestamp = lastEditTimestamp, + isDeleted = deleted, + referenceId = referenceId, + isTemporary = isTemporary, + sendStatus = sendStatus, + readStatus = ReadStatus.NONE, + silent = silent, + threadTitle = threadTitle, + threadReplies = threadReplies + ) + +fun ChatMessageJson.asModel() = + ChatMessage( + jsonMessageId = id.toInt(), + message = message, + token = token, + threadId = threadId, + isThread = hasThread, + actorType = actorType, + actorId = actorId, + actorDisplayName = actorDisplayName, + timestamp = timestamp, + messageParameters = messageParameters, + systemMessageType = systemMessageType, + replyable = replyable, + parentMessageId = parentMessage?.id, + messageType = messageType, + reactions = reactions, + reactionsSelf = reactionsSelf, + expirationTimestamp = expirationTimestamp, + renderMarkdown = renderMarkdown, + lastEditActorDisplayName = lastEditActorDisplayName, + lastEditActorId = lastEditActorId, + lastEditActorType = lastEditActorType, + lastEditTimestamp = lastEditTimestamp, + isDeleted = deleted, + referenceId = referenceId, + silent = silent, + threadTitle = threadTitle, + threadReplies = threadReplies + ) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt new file mode 100644 index 0000000..0953376 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/mappers/ConversationMapUtils.kt @@ -0,0 +1,176 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.mappers + +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.models.json.conversations.Conversation + +fun ConversationModel.asEntity() = + ConversationEntity( + internalId = internalId, + accountId = accountId, + token = token, + name = name, + displayName = displayName, + description = description, + type = type, + lastPing = lastPing, + participantType = participantType, + hasPassword = hasPassword, + sessionId = sessionId, + actorId = actorId, + actorType = actorType, + favorite = favorite, + lastActivity = lastActivity, + unreadMessages = unreadMessages, + unreadMention = unreadMention, + lastMessage = lastMessage?.let { LoganSquare.serialize(lastMessage) }, + objectType = objectType, + objectId = objectId, + notificationLevel = notificationLevel, + conversationReadOnlyState = conversationReadOnlyState, + lobbyState = lobbyState, + lobbyTimer = lobbyTimer, + lastReadMessage = lastReadMessage, + lastCommonReadMessage = lastCommonReadMessage, + hasCall = hasCall, + callFlag = callFlag, + canStartCall = canStartCall, + canLeaveConversation = canLeaveConversation, + canDeleteConversation = canDeleteConversation, + unreadMentionDirect = unreadMentionDirect, + notificationCalls = notificationCalls, + permissions = permissions, + messageExpiration = messageExpiration, + status = status, + statusIcon = statusIcon, + statusMessage = statusMessage, + statusClearAt = statusClearAt, + callRecording = callRecording, + avatarVersion = avatarVersion, + hasCustomAvatar = hasCustomAvatar, + callStartTime = callStartTime, + recordingConsentRequired = recordingConsentRequired, + remoteServer = remoteServer, + remoteToken = remoteToken, + hasArchived = hasArchived, + hasSensitive = hasSensitive, + hasImportant = hasImportant, + messageDraft = messageDraft + ) + +fun ConversationEntity.asModel() = + ConversationModel( + internalId = internalId, + accountId = accountId, + token = token, + name = name, + displayName = displayName, + description = description, + type = type, + lastPing = lastPing, + participantType = participantType, + hasPassword = hasPassword, + sessionId = sessionId, + actorId = actorId, + actorType = actorType, + favorite = favorite, + lastActivity = lastActivity, + unreadMessages = unreadMessages, + unreadMention = unreadMention, + lastMessage = lastMessage?.let + { LoganSquare.parse(lastMessage, ChatMessageJson::class.java) }, + objectType = objectType, + objectId = objectId, + notificationLevel = notificationLevel, + conversationReadOnlyState = conversationReadOnlyState, + lobbyState = lobbyState, + lobbyTimer = lobbyTimer, + lastReadMessage = lastReadMessage, + lastCommonReadMessage = lastCommonReadMessage, + hasCall = hasCall, + callFlag = callFlag, + canStartCall = canStartCall, + canLeaveConversation = canLeaveConversation, + canDeleteConversation = canDeleteConversation, + unreadMentionDirect = unreadMentionDirect, + notificationCalls = notificationCalls, + permissions = permissions, + messageExpiration = messageExpiration, + status = status, + statusIcon = statusIcon, + statusMessage = statusMessage, + statusClearAt = statusClearAt, + callRecording = callRecording, + avatarVersion = avatarVersion, + hasCustomAvatar = hasCustomAvatar, + callStartTime = callStartTime, + recordingConsentRequired = recordingConsentRequired, + remoteServer = remoteServer, + remoteToken = remoteToken, + hasArchived = hasArchived, + hasSensitive = hasSensitive, + hasImportant = hasImportant, + messageDraft = messageDraft + ) + +fun Conversation.asEntity(accountId: Long) = + ConversationEntity( + internalId = "$accountId@$token", + accountId = accountId, + token = token, + name = name, + displayName = displayName, + description = description, + type = type, + lastPing = lastPing, + participantType = participantType, + hasPassword = hasPassword, + sessionId = sessionId, + actorId = actorId, + actorType = actorType, + favorite = favorite, + lastActivity = lastActivity, + unreadMessages = unreadMessages, + unreadMention = unreadMention, + lastMessage = lastMessage?.let { LoganSquare.serialize(lastMessage) }, + objectType = objectType, + objectId = objectId, + notificationLevel = notificationLevel, + conversationReadOnlyState = conversationReadOnlyState, + lobbyState = lobbyState, + lobbyTimer = lobbyTimer, + lastReadMessage = lastReadMessage, + lastCommonReadMessage = lastCommonReadMessage, + hasCall = hasCall, + callFlag = callFlag, + canStartCall = canStartCall, + canLeaveConversation = canLeaveConversation, + canDeleteConversation = canDeleteConversation, + unreadMentionDirect = unreadMentionDirect, + notificationCalls = notificationCalls, + permissions = permissions, + messageExpiration = messageExpiration, + status = status, + statusIcon = statusIcon, + statusMessage = statusMessage, + statusClearAt = statusClearAt, + callRecording = callRecording, + avatarVersion = avatarVersion, + hasCustomAvatar = hasCustomAvatar, + callStartTime = callStartTime, + recordingConsentRequired = recordingConsentRequired, + remoteServer = remoteServer, + remoteToken = remoteToken, + hasArchived = hasArchived, + hasSensitive = hasSensitive, + hasImportant = hasImportant + ) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt new file mode 100644 index 0000000..62ec1b4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatBlockEntity.kt @@ -0,0 +1,42 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "ChatBlocks", + foreignKeys = [ + ForeignKey( + entity = ConversationEntity::class, + parentColumns = arrayOf("internalId"), + childColumns = arrayOf("internalConversationId"), + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE + ) + ], + indices = [ + Index(value = ["internalConversationId"]) + ] +) +data class ChatBlockEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") var id: Long = 0, + // accountId@token + @ColumnInfo(name = "internalConversationId") var internalConversationId: String, + @ColumnInfo(name = "accountId") var accountId: Long? = null, + @ColumnInfo(name = "token") var token: String?, + @ColumnInfo(name = "threadId") var threadId: Long? = null, + @ColumnInfo(name = "oldestMessageId") var oldestMessageId: Long, + @ColumnInfo(name = "newestMessageId") var newestMessageId: Long, + @ColumnInfo(name = "hasHistory") var hasHistory: Boolean +) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt new file mode 100644 index 0000000..3fc2bb0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ChatMessageEntity.kt @@ -0,0 +1,74 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import com.nextcloud.talk.chat.data.model.ChatMessage + +@Entity( + tableName = "ChatMessages", + foreignKeys = [ + ForeignKey( + entity = ConversationEntity::class, + parentColumns = arrayOf("internalId"), + childColumns = arrayOf("internalConversationId"), + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE + ) + ], + indices = [ + Index(value = ["internalId"], unique = true), + Index(value = ["internalConversationId"]) + ] +) +data class ChatMessageEntity( + // MOST IMPORTANT ATTRIBUTES + + @PrimaryKey + // accountId@roomtoken@messageId + @ColumnInfo(name = "internalId") var internalId: String, + @ColumnInfo(name = "accountId") var accountId: Long, + @ColumnInfo(name = "token") var token: String, + @ColumnInfo(name = "id") var id: Long = 0, + // accountId@roomtoken + @ColumnInfo(name = "internalConversationId") var internalConversationId: String, + @ColumnInfo(name = "threadId") var threadId: Long? = null, + @ColumnInfo(name = "isThread") var isThread: Boolean = false, + @ColumnInfo(name = "actorDisplayName") var actorDisplayName: String, + @ColumnInfo(name = "message") var message: String, + + // OTHER ATTRIBUTES IN ALPHABETICAL ORDER + + @ColumnInfo(name = "actorId") var actorId: String, + @ColumnInfo(name = "actorType") var actorType: String, + @ColumnInfo(name = "deleted") var deleted: Boolean = false, + @ColumnInfo(name = "expirationTimestamp") var expirationTimestamp: Int = 0, + @ColumnInfo(name = "isReplyable") var replyable: Boolean = false, + @ColumnInfo(name = "isTemporary") var isTemporary: Boolean = false, + @ColumnInfo(name = "lastEditActorDisplayName") var lastEditActorDisplayName: String? = null, + @ColumnInfo(name = "lastEditActorId") var lastEditActorId: String? = null, + @ColumnInfo(name = "lastEditActorType") var lastEditActorType: String? = null, + @ColumnInfo(name = "lastEditTimestamp") var lastEditTimestamp: Long? = 0, + @ColumnInfo(name = "markdown") var renderMarkdown: Boolean? = false, + @ColumnInfo(name = "messageParameters") var messageParameters: HashMap>? = null, + @ColumnInfo(name = "messageType") var messageType: String, + @ColumnInfo(name = "parent") var parentMessageId: Long? = null, + @ColumnInfo(name = "reactions") var reactions: LinkedHashMap? = null, + @ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList? = null, + @ColumnInfo(name = "referenceId") var referenceId: String? = null, + @ColumnInfo(name = "sendStatus") var sendStatus: SendStatus? = null, + @ColumnInfo(name = "silent") var silent: Boolean = false, + @ColumnInfo(name = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType, + @ColumnInfo(name = "threadTitle") var threadTitle: String? = null, + @ColumnInfo(name = "threadReplies") var threadReplies: Int? = 0, + @ColumnInfo(name = "timestamp") var timestamp: Long = 0 +) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt new file mode 100644 index 0000000..8301b8c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/ConversationEntity.kt @@ -0,0 +1,117 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import com.nextcloud.talk.data.user.model.UserEntity +import com.nextcloud.talk.models.MessageDraft +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant + +@Entity( + tableName = "Conversations", + foreignKeys = [ + ForeignKey( + entity = UserEntity::class, + parentColumns = arrayOf("id"), + childColumns = arrayOf("accountId"), + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE + ) + ], + indices = [ + Index(value = ["accountId"]) + ] +) +data class ConversationEntity( + // MOST IMPORTANT ATTRIBUTES + + @PrimaryKey + @ColumnInfo(name = "internalId") + var internalId: String, + + // Defines to which talk app account this conversation belongs to + @ColumnInfo(name = "accountId") var accountId: Long, + + // We don't use token as primary key as we have to manage multiple talk app accounts on + // the phone, thus multiple accounts can have the same conversation in their list. That's why the servers + // conversation token is not suitable as primary key on the phone. Also the conversation attributes such as + // "unread message" etc only match a specific account. + // If multiple talk app accounts have the same conversation, it is stored as another dataset, which is + // exactly what we want for this case. + @ColumnInfo(name = "token") var token: String, + + @ColumnInfo(name = "displayName") var displayName: String, + + // OTHER ATTRIBUTES IN ALPHABETICAL ORDER + @ColumnInfo(name = "actorId") var actorId: String, + @ColumnInfo(name = "actorType") var actorType: String, + @ColumnInfo(name = "avatarVersion") var avatarVersion: String, + @ColumnInfo(name = "callFlag") var callFlag: Int = 0, + @ColumnInfo(name = "callRecording") var callRecording: Int = 0, + @ColumnInfo(name = "callStartTime") var callStartTime: Long = 0, + @ColumnInfo(name = "canDeleteConversation") var canDeleteConversation: Boolean, + @ColumnInfo(name = "canLeaveConversation") var canLeaveConversation: Boolean, + @ColumnInfo(name = "canStartCall") var canStartCall: Boolean = false, + @ColumnInfo(name = "description") var description: String, + @ColumnInfo(name = "hasCall") var hasCall: Boolean = false, + @ColumnInfo(name = "hasPassword") var hasPassword: Boolean = false, + @ColumnInfo(name = "isCustomAvatar") var hasCustomAvatar: Boolean, + @ColumnInfo(name = "isFavorite") var favorite: Boolean = false, + @ColumnInfo(name = "lastActivity") var lastActivity: Long = 0, + @ColumnInfo(name = "lastCommonReadMessage") var lastCommonReadMessage: Int = 0, + @ColumnInfo(name = "lastMessage") var lastMessage: String? = null, + @ColumnInfo(name = "lastPing") var lastPing: Long = 0, + @ColumnInfo(name = "lastReadMessage") var lastReadMessage: Int = 0, + @ColumnInfo(name = "lobbyState") var lobbyState: ConversationEnums.LobbyState, + @ColumnInfo(name = "lobbyTimer") var lobbyTimer: Long = 0, + @ColumnInfo(name = "messageExpiration") var messageExpiration: Int = 0, + @ColumnInfo(name = "name") var name: String, + @ColumnInfo(name = "notificationCalls") var notificationCalls: Int = 0, + @ColumnInfo(name = "notificationLevel") var notificationLevel: ConversationEnums.NotificationLevel, + @ColumnInfo(name = "objectType") var objectType: ConversationEnums.ObjectType, + @ColumnInfo(name = "objectId") var objectId: String, + @ColumnInfo(name = "participantType") var participantType: Participant.ParticipantType, + @ColumnInfo(name = "permissions") var permissions: Int = 0, + @ColumnInfo(name = "readOnly") var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState, + @ColumnInfo(name = "recordingConsent") var recordingConsentRequired: Int = 0, + @ColumnInfo(name = "remoteServer") var remoteServer: String? = null, + @ColumnInfo(name = "remoteToken") var remoteToken: String? = null, + @ColumnInfo(name = "sessionId") var sessionId: String, + @ColumnInfo(name = "status") var status: String? = null, + @ColumnInfo(name = "statusClearAt") var statusClearAt: Long? = 0, + @ColumnInfo(name = "statusIcon") var statusIcon: String? = null, + @ColumnInfo(name = "statusMessage") var statusMessage: String? = null, + @ColumnInfo(name = "type") var type: ConversationEnums.ConversationType, + @ColumnInfo(name = "unreadMention") var unreadMention: Boolean = false, + @ColumnInfo(name = "unreadMentionDirect") var unreadMentionDirect: Boolean, + @ColumnInfo(name = "unreadMessages") var unreadMessages: Int = 0, + @ColumnInfo(name = "hasArchived") var hasArchived: Boolean = false, + @ColumnInfo(name = "hasSensitive") var hasSensitive: Boolean = false, + @ColumnInfo(name = "hasImportant") var hasImportant: Boolean = false, + @ColumnInfo(name = "messageDraft") var messageDraft: MessageDraft? = MessageDraft() + // missing/not needed: attendeeId + // missing/not needed: attendeePin + // missing/not needed: attendeePermissions + // missing/not needed: callPermissions + // missing/not needed: defaultPermissions + // missing/not needed: participantInCall + // missing/not needed: participantFlags + // missing/not needed: listable + // missing/not needed: count + // missing/not needed: numGuests + // missing/not needed: sipEnabled + // missing/not needed: canEnableSIP + // missing/not needed: objectId + // missing/not needed: breakoutRoomMode + // missing/not needed: breakoutRoomStatus +) diff --git a/app/src/main/java/com/nextcloud/talk/data/database/model/SendStatus.kt b/app/src/main/java/com/nextcloud/talk/data/database/model/SendStatus.kt new file mode 100644 index 0000000..6d18227 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/database/model/SendStatus.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.database.model + +enum class SendStatus { + PENDING, + SENT_PENDING_ACK, + FAILED +} diff --git a/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitor.kt b/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitor.kt new file mode 100644 index 0000000..f8d4548 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitor.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.network + +import androidx.lifecycle.LiveData +import kotlinx.coroutines.flow.StateFlow + +/** + * Utility for reporting app connectivity status. + */ +interface NetworkMonitor { + /** + * Returns the device's current connectivity status. + */ + val isOnline: StateFlow + + /** + * Returns the device's current connectivity status as LiveData for better interop with Java code. + */ + val isOnlineLiveData: LiveData +} diff --git a/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitorImpl.kt b/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitorImpl.kt new file mode 100644 index 0000000..5b9f6ad --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/network/NetworkMonitorImpl.kt @@ -0,0 +1,84 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.util.Log +import androidx.core.content.getSystemService +import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NetworkMonitorImpl @Inject constructor(private val context: Context) : NetworkMonitor { + + private val connectivityManager = context.getSystemService()!! + + override val isOnlineLiveData: LiveData + get() = isOnline.asLiveData() + + override val isOnline: StateFlow get() = _isOnline + + private val _isOnline: StateFlow = callbackFlow { + val callback = object : ConnectivityManager.NetworkCallback() { + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + super.onCapabilitiesChanged(network, networkCapabilities) + val connected = networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_VALIDATED + ) + trySend(connected) + Log.d(TAG, "Network status changed: $connected") + } + + override fun onUnavailable() { + super.onUnavailable() + trySend(false) + Log.d(TAG, "Network status: onUnavailable") + } + + override fun onLost(network: Network) { + super.onLost(network) + trySend(false) + Log.d(TAG, "Network status: onLost") + } + + override fun onAvailable(network: Network) { + super.onAvailable(network) + trySend(true) + Log.d(TAG, "Network status: onAvailable") + } + } + + connectivityManager.registerDefaultNetworkCallback(callback) + + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + } + }.stateIn( + CoroutineScope(Dispatchers.IO), + SharingStarted.WhileSubscribed(COROUTINE_TIMEOUT), + false + ) + + companion object { + private val TAG = NetworkMonitorImpl::class.java.simpleName + private const val COROUTINE_TIMEOUT = 5000L + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt new file mode 100644 index 0000000..dc5e8ac --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt @@ -0,0 +1,421 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024-2025 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.source.local + +import android.util.Log +import androidx.room.DeleteColumn +import androidx.room.migration.AutoMigrationSpec +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import java.sql.SQLException + +@Suppress("MagicNumber") +object Migrations { + + //region Auto migrations + + @DeleteColumn(tableName = "ChatMessages", columnName = "sendingFailed") + class AutoMigration16To17 : AutoMigrationSpec + + //endregion + + //region Manual migrations + + val MIGRATION_6_8 = object : Migration(6, 8) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 6 to 8") + migrateToRoom(db) + } + } + + val MIGRATION_7_8 = object : Migration(7, 8) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 7 to 8") + migrateToRoom(db) + } + } + + val MIGRATION_8_9 = object : Migration(8, 9) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 8 to 9") + migrateToDualPrimaryKeyArbitraryStorage(db) + } + } + + val MIGRATION_10_11 = object : Migration(10, 11) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 10 to 11") + migrateToOfflineSupport(db) + } + } + + val MIGRATION_11_12 = object : Migration(11, 12) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 11 to 12") + addArchiveConversations(db) + } + } + + val MIGRATION_12_13 = object : Migration(12, 13) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 12 to 13") + addTempMessagesSupport(db) + } + } + + val MIGRATION_13_14 = object : Migration(13, 14) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 13 to 14") + addObjectId(db) + } + } + + val MIGRATION_14_15 = object : Migration(14, 15) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 14 to 15") + addIsSensitive(db) + } + } + + val MIGRATION_15_16 = object : Migration(15, 16) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 15 to 16") + addIsImportant(db) + } + } + + val MIGRATION_17_19 = object : Migration(17, 19) { + override fun migrate(db: SupportSQLiteDatabase) { + Log.i( + "Migrations", + "Migrating 17 to 19 (migration 17 to 18 had bugs in app version v22.0.0 Alpha 11 and " + + "v22.0.0 Alpha 12)" + ) + migrateToMessageThreads(db) + } + } + + //endregion + + fun migrateToRoom(db: SupportSQLiteDatabase) { + db.execSQL( + "CREATE TABLE User_new (" + + "id INTEGER NOT NULL, " + + "userId TEXT, " + + "username TEXT, " + + "baseUrl TEXT, " + + "token TEXT, " + + "displayName TEXT, " + + "pushConfigurationState TEXT, " + + "capabilities TEXT, " + + "clientCertificate TEXT, " + + "externalSignalingServer TEXT, " + + "current INTEGER NOT NULL, " + + "scheduledForDeletion INTEGER NOT NULL, " + + "PRIMARY KEY(id)" + + ")" + ) + + db.execSQL( + "CREATE TABLE ArbitraryStorage_new (" + + "accountIdentifier INTEGER NOT NULL, " + + "\"key\" TEXT, " + + "object TEXT, " + + "value TEXT, " + + "PRIMARY KEY(accountIdentifier)" + + ")" + ) + // Copy the data + db.execSQL( + "INSERT INTO User_new (" + + "id, userId, username, baseUrl, token, displayName, pushConfigurationState, capabilities, " + + "clientCertificate, externalSignalingServer, current, scheduledForDeletion) " + + "SELECT " + + "id, userId, username, baseUrl, token, displayName, pushConfigurationState, capabilities, " + + "clientCertificate, externalSignalingServer, current, scheduledForDeletion " + + "FROM User" + ) + db.execSQL( + "INSERT INTO ArbitraryStorage_new (" + + "accountIdentifier, \"key\", object, value) " + + "SELECT " + + "accountIdentifier, \"key\", object, value " + + "FROM ArbitraryStorage" + ) + // Remove the old table + db.execSQL("DROP TABLE User") + db.execSQL("DROP TABLE ArbitraryStorage") + + // Change the table name to the correct one + db.execSQL("ALTER TABLE User_new RENAME TO User") + db.execSQL("ALTER TABLE ArbitraryStorage_new RENAME TO ArbitraryStorage") + } + + fun migrateToDualPrimaryKeyArbitraryStorage(db: SupportSQLiteDatabase) { + db.execSQL( + "CREATE TABLE ArbitraryStorage_dualPK (" + + "accountIdentifier INTEGER NOT NULL, " + + "\"key\" TEXT NOT NULL, " + + "object TEXT, " + + "value TEXT, " + + "PRIMARY KEY(accountIdentifier, \"key\")" + + ")" + ) + // Copy the data + db.execSQL( + "INSERT INTO ArbitraryStorage_dualPK (" + + "accountIdentifier, \"key\", object, value) " + + "SELECT " + + "accountIdentifier, \"key\", object, value " + + "FROM ArbitraryStorage" + ) + // Remove the old table + db.execSQL("DROP TABLE ArbitraryStorage") + + // Change the table name to the correct one + db.execSQL("ALTER TABLE ArbitraryStorage_dualPK RENAME TO ArbitraryStorage") + } + + @Suppress("Detekt.LongMethod") + fun migrateToOfflineSupport(db: SupportSQLiteDatabase) { + db.execSQL( + "CREATE TABLE IF NOT EXISTS Conversations (" + + "`internalId` TEXT NOT NULL, " + + "`accountId` INTEGER NOT NULL, " + + "`token` TEXT NOT NULL, " + + "`displayName` TEXT NOT NULL, " + + "`actorId` TEXT NOT NULL, " + + "`actorType` TEXT NOT NULL, " + + "`avatarVersion` TEXT NOT NULL, " + + "`callFlag` INTEGER NOT NULL, " + + "`callRecording` INTEGER NOT NULL, " + + "`callStartTime` INTEGER NOT NULL, " + + "`canDeleteConversation` INTEGER NOT NULL, " + + "`canLeaveConversation` INTEGER NOT NULL, " + + "`canStartCall` INTEGER NOT NULL, " + + "`description` TEXT NOT NULL, " + + "`hasCall` INTEGER NOT NULL, " + + "`hasPassword` INTEGER NOT NULL, " + + "`isCustomAvatar` INTEGER NOT NULL, " + + "`isFavorite` INTEGER NOT NULL, " + + "`lastActivity` INTEGER NOT NULL, " + + "`lastCommonReadMessage` INTEGER NOT NULL, " + + "`lastMessage` TEXT, " + + "`lastPing` INTEGER NOT NULL, " + + "`lastReadMessage` INTEGER NOT NULL, " + + "`lobbyState` TEXT NOT NULL, " + + "`lobbyTimer` INTEGER NOT NULL, " + + "`messageExpiration` INTEGER NOT NULL, " + + "`name` TEXT NOT NULL, " + + "`notificationCalls` INTEGER NOT NULL, " + + "`notificationLevel` TEXT NOT NULL, " + + "`objectType` TEXT NOT NULL, " + + "`participantType` TEXT NOT NULL, " + + "`permissions` INTEGER NOT NULL, " + + "`readOnly` TEXT NOT NULL, " + + "`recordingConsent` INTEGER NOT NULL, " + + "`remoteServer` TEXT, " + + "`remoteToken` TEXT, " + + "`sessionId` TEXT NOT NULL, " + + "`status` TEXT, " + + "`statusClearAt` INTEGER, " + + "`statusIcon` TEXT, " + + "`statusMessage` TEXT, " + + "`type` TEXT NOT NULL, " + + "`unreadMention` INTEGER NOT NULL, " + + "`unreadMentionDirect` INTEGER NOT NULL, " + + "`unreadMessages` INTEGER NOT NULL, " + + "PRIMARY KEY(`internalId`), " + + "FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) " + + "ON UPDATE CASCADE ON DELETE CASCADE " + + ")" + ) + + db.execSQL( + "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `Conversations` (`accountId`)" + ) + + db.execSQL( + "CREATE TABLE IF NOT EXISTS ChatMessages (" + + "`internalId` TEXT NOT NULL, " + + "`accountId` INTEGER NOT NULL, " + + "`token` TEXT NOT NULL, " + + "`id` INTEGER NOT NULL, " + + "`internalConversationId` TEXT NOT NULL, " + + "`actorDisplayName` TEXT NOT NULL, " + + "`message` TEXT NOT NULL, " + + "`actorId` TEXT NOT NULL, " + + "`actorType` TEXT NOT NULL, " + + "`deleted` INTEGER NOT NULL, " + + "`expirationTimestamp` INTEGER NOT NULL, " + + "`isReplyable` INTEGER NOT NULL, " + + "`lastEditActorDisplayName` TEXT, " + + "`lastEditActorId` TEXT, " + + "`lastEditActorType` TEXT, " + + "`lastEditTimestamp` INTEGER, " + + "`markdown` INTEGER, " + + "`messageParameters` TEXT, " + + "`messageType` TEXT NOT NULL, " + + "`parent` INTEGER, " + + "`reactions` TEXT, " + + "`reactionsSelf` TEXT, " + + "`systemMessage` TEXT NOT NULL, " + + "`timestamp` INTEGER NOT NULL, " + + "PRIMARY KEY(`internalId`), " + + "FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) " + + "ON UPDATE CASCADE ON DELETE CASCADE " + + ")" + ) + + db.execSQL( + "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` " + + "ON `ChatMessages` (`internalId`)" + ) + + db.execSQL( + "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` " + + "ON `ChatMessages` (`internalConversationId`)" + ) + + db.execSQL( + "CREATE TABLE IF NOT EXISTS ChatBlocks (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`internalConversationId` TEXT NOT NULL, " + + "`accountId` INTEGER, `token` TEXT, " + + "`oldestMessageId` INTEGER NOT NULL, " + + "`newestMessageId` INTEGER NOT NULL, " + + "`hasHistory` INTEGER NOT NULL, " + + "FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) " + + "ON UPDATE CASCADE ON DELETE CASCADE " + + ")" + ) + + db.execSQL( + "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` " + + "ON `ChatBlocks` (`internalConversationId`)" + ) + } + + fun addArchiveConversations(db: SupportSQLiteDatabase) { + try { + db.execSQL( + "ALTER TABLE Conversations " + + "ADD COLUMN hasArchived INTEGER NOT NULL DEFAULT 0;" + ) + } catch (e: SQLException) { + Log.i("Migrations", "hasArchived already exists", e) + } + } + + fun addObjectId(db: SupportSQLiteDatabase) { + try { + db.execSQL( + "ALTER TABLE Conversations " + + "ADD COLUMN objectId TEXT NOT NULL DEFAULT '';" + ) + } catch (e: SQLException) { + Log.i("Migrations", "Something went wrong when adding column objectId to table Conversations", e) + } + } + + fun addIsSensitive(db: SupportSQLiteDatabase) { + try { + db.execSQL( + "ALTER TABLE Conversations " + + "ADD COLUMN hasSensitive INTEGER NOT NULL DEFAULT 0;" + ) + } catch (e: SQLException) { + Log.i("Migrations", "Something went wrong when adding column hasSensitive to table Conversations", e) + } + } + + fun addIsImportant(db: SupportSQLiteDatabase) { + try { + db.execSQL( + "ALTER TABLE Conversations " + + "ADD COLUMN hasImportant INTEGER NOT NULL DEFAULT 0;" + ) + } catch (e: SQLException) { + Log.i("Migrations", "Something went wrong when adding column hasImportant to table Conversations", e) + } + } + + fun migrateToMessageThreads(db: SupportSQLiteDatabase) { + try { + db.execSQL( + "ALTER TABLE ChatBlocks " + + "ADD COLUMN threadId INTEGER;" + ) + + db.execSQL( + "ALTER TABLE ChatMessages " + + "ADD COLUMN threadId INTEGER;" + ) + + db.execSQL( + "ALTER TABLE ChatMessages " + + "ADD COLUMN isThread INTEGER NOT NULL DEFAULT 0;" + ) + + // Foreign key constraints are not active during migration. + // At least db.execSQL("PRAGMA foreign_keys=ON;") etc did not help. + // Because of this it is not enough to just clear the Conversations table (to have cascade deletion in + // other tables), but all related tables have to be cleared with SQL statement as well. + + db.execSQL( + "DELETE FROM Conversations" + ) + db.execSQL( + "DELETE FROM ChatMessages" + ) + db.execSQL( + "DELETE FROM ChatBlocks" + ) + } catch (e: SQLException) { + Log.i("Migrations", "Something went wrong when migrating to messageThreads", e) + } + } + + fun addTempMessagesSupport(db: SupportSQLiteDatabase) { + try { + db.execSQL( + "ALTER TABLE ChatMessages " + + "ADD COLUMN referenceId TEXT;" + ) + } catch (e: SQLException) { + Log.i("Migrations", "Something went wrong when adding column referenceId to table ChatMessages", e) + } + + try { + db.execSQL( + "ALTER TABLE ChatMessages " + + "ADD COLUMN isTemporary INTEGER NOT NULL DEFAULT 0;" + ) + } catch (e: SQLException) { + Log.i("Migrations", "Something went wrong when adding column isTemporary to table ChatMessages", e) + } + + try { + db.execSQL( + "ALTER TABLE ChatMessages " + + "ADD COLUMN sendingFailed INTEGER NOT NULL DEFAULT 0;" + ) + } catch (e: SQLException) { + Log.i("Migrations", "Something went wrong when adding column sendingFailed to table ChatMessages", e) + } + + try { + db.execSQL( + "ALTER TABLE ChatMessages " + + "ADD COLUMN silent INTEGER NOT NULL DEFAULT 0;" + ) + } catch (e: SQLException) { + Log.i("Migrations", "Something went wrong when adding column silent to table ChatMessages", e) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt new file mode 100644 index 0000000..dff97d1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt @@ -0,0 +1,143 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023-2025 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.source.local + +import android.content.Context +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.sqlite.db.SupportSQLiteDatabase +import com.nextcloud.talk.R +import com.nextcloud.talk.data.database.dao.ChatBlocksDao +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.database.dao.ConversationsDao +import com.nextcloud.talk.data.database.model.ChatBlockEntity +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.data.source.local.Migrations.AutoMigration16To17 +import com.nextcloud.talk.data.source.local.converters.ArrayListConverter +import com.nextcloud.talk.data.source.local.converters.CapabilitiesConverter +import com.nextcloud.talk.data.source.local.converters.ExternalSignalingServerConverter +import com.nextcloud.talk.data.source.local.converters.HashMapHashMapConverter +import com.nextcloud.talk.data.source.local.converters.LinkedHashMapConverter +import com.nextcloud.talk.data.source.local.converters.PushConfigurationConverter +import com.nextcloud.talk.data.source.local.converters.SendStatusConverter +import com.nextcloud.talk.data.source.local.converters.ServerVersionConverter +import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter +import com.nextcloud.talk.data.storage.ArbitraryStoragesDao +import com.nextcloud.talk.data.storage.model.ArbitraryStorageEntity +import com.nextcloud.talk.data.user.UsersDao +import com.nextcloud.talk.data.user.model.UserEntity +import com.nextcloud.talk.models.MessageDraftConverter +import net.zetetic.database.sqlcipher.SupportOpenHelperFactory +import java.util.Locale + +@Database( + entities = [ + UserEntity::class, + ArbitraryStorageEntity::class, + ConversationEntity::class, + ChatMessageEntity::class, + ChatBlockEntity::class + ], + version = 21, + autoMigrations = [ + AutoMigration(from = 9, to = 10), + AutoMigration(from = 16, to = 17, spec = AutoMigration16To17::class), + AutoMigration(from = 19, to = 20), + AutoMigration(from = 20, to = 21) + ], + exportSchema = true +) +@TypeConverters( + PushConfigurationConverter::class, + CapabilitiesConverter::class, + ServerVersionConverter::class, + ExternalSignalingServerConverter::class, + SignalingSettingsConverter::class, + HashMapHashMapConverter::class, + LinkedHashMapConverter::class, + ArrayListConverter::class, + SendStatusConverter::class, + MessageDraftConverter::class +) +@Suppress("MagicNumber") +abstract class TalkDatabase : RoomDatabase() { + abstract fun usersDao(): UsersDao + abstract fun conversationsDao(): ConversationsDao + abstract fun chatMessagesDao(): ChatMessagesDao + abstract fun chatBlocksDao(): ChatBlocksDao + abstract fun arbitraryStoragesDao(): ArbitraryStoragesDao + + companion object { + const val TAG = "TalkDatabase" + const val SQL_CIPHER_LIBRARY = "sqlcipher" + + @Volatile + private var instance: TalkDatabase? = null + + @JvmStatic + fun getInstance(context: Context): TalkDatabase = + instance ?: synchronized(this) { + instance ?: build(context).also { instance = it } + } + + // If editing the migrations, please add a test case in MigrationsTest under androidTest/data + val MIGRATIONS = arrayOf( + Migrations.MIGRATION_6_8, + Migrations.MIGRATION_7_8, + Migrations.MIGRATION_8_9, + Migrations.MIGRATION_10_11, + Migrations.MIGRATION_11_12, + Migrations.MIGRATION_12_13, + Migrations.MIGRATION_13_14, + Migrations.MIGRATION_14_15, + Migrations.MIGRATION_15_16, + Migrations.MIGRATION_17_19 + ) + + @Suppress("SpreadOperator") + private fun build(context: Context): TalkDatabase { + val passCharArray = context.getString(R.string.nc_talk_database_encryption_key).toCharArray() + val passphrase: ByteArray = getBytesFromChars(passCharArray) + val factory = SupportOpenHelperFactory(passphrase) + + val dbName = context + .resources + .getString(R.string.nc_app_product_name) + .lowercase(Locale.getDefault()) + .replace(" ", "_") + .trim() + + ".sqlite" + + System.loadLibrary(SQL_CIPHER_LIBRARY) + + return Room + .databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName) + // comment out openHelperFactory to view the database entries in Android Studio for debugging + .openHelperFactory(factory) + .fallbackToDestructiveMigrationFrom(true, 18) + .addMigrations(*MIGRATIONS) // * converts migrations to vararg + .allowMainThreadQueries() + .addCallback( + object : Callback() { + override fun onOpen(db: SupportSQLiteDatabase) { + super.onOpen(db) + db.execSQL("PRAGMA defer_foreign_keys = 1") + } + } + ) + .build() + } + + private fun getBytesFromChars(chars: CharArray): ByteArray = String(chars).toByteArray(Charsets.UTF_8) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ArrayListConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ArrayListConverter.kt new file mode 100644 index 0000000..1b3aecc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ArrayListConverter.kt @@ -0,0 +1,39 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.source.local.converters + +import android.util.Log +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare + +class ArrayListConverter { + + @Suppress("Detekt.TooGenericExceptionCaught") + @TypeConverter + fun arrayListToString(list: ArrayList?): String? { + return if (list == null) { + null + } else { + return try { + LoganSquare.serialize(list) + } catch (e: Exception) { + Log.e("ArrayListConverter", "Error parsing array list $list to String $e") + "" + } + } + } + + @TypeConverter + fun stringToArrayList(value: String?): ArrayList? { + if (value.isNullOrEmpty()) { + return null + } + + return LoganSquare.parseList(value, String::class.java) as ArrayList? + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/CapabilitiesConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/CapabilitiesConverter.kt new file mode 100644 index 0000000..26134cf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/CapabilitiesConverter.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.models.json.capabilities.Capabilities + +class CapabilitiesConverter { + @TypeConverter + fun fromCapabilitiesToString(capabilities: Capabilities?): String = + if (capabilities == null) { + "" + } else { + LoganSquare.serialize(capabilities) + } + + @TypeConverter + fun fromStringToCapabilities(value: String): Capabilities? { + return if (value.isBlank()) { + null + } else { + return LoganSquare.parse(value, Capabilities::class.java) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ExternalSignalingServerConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ExternalSignalingServerConverter.kt new file mode 100644 index 0000000..7e3f794 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ExternalSignalingServerConverter.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.models.ExternalSignalingServer + +class ExternalSignalingServerConverter { + + @TypeConverter + fun fromExternalSignalingServerToString(externalSignalingServer: ExternalSignalingServer?): String = + if (externalSignalingServer == null) { + "" + } else { + LoganSquare.serialize(externalSignalingServer) + } + + @TypeConverter + fun fromStringToExternalSignalingServer(value: String): ExternalSignalingServer? { + return if (value.isBlank()) { + null + } else { + return LoganSquare.parse(value, ExternalSignalingServer::class.java) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt new file mode 100644 index 0000000..3e0a10c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare + +class HashMapHashMapConverter { + @TypeConverter + fun fromDoubleHashMapToString(map: HashMap>?): String? { + return if (map == null) { + LoganSquare.serialize(hashMapOf>()) + } else { + return LoganSquare.serialize(map) + } + } + + @TypeConverter + fun fromStringToDoubleHashMap(value: String?): HashMap>? { + if (value.isNullOrEmpty()) { + return hashMapOf() + } + + return LoganSquare.parseMap(value, HashMap::class.java) as HashMap>? + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapConverter.kt new file mode 100644 index 0000000..c6abcc8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapConverter.kt @@ -0,0 +1,58 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.source.local.converters + +import android.util.Log +import androidx.room.TypeConverter +import com.fasterxml.jackson.core.JsonFactory +import java.io.IOException + +class LinkedHashMapConverter { + + private val converter = LinkedHashMapStringIntConverter() + private val jsonFactory = JsonFactory() + + @TypeConverter + fun stringToLinkedHashMap(value: String?): LinkedHashMap { + if (value.isNullOrEmpty() || value == "{}") { + return linkedMapOf() + } + // "{"👍":1,"👎":1,"😃":1,"😯":1}" // pretend this is value + return try { + val map = linkedMapOf() + val trimmed = value.replace("{", "").replace("}", "") + // "👍":1,"👎":1,"😃":1,"😯":1 + val mapList = trimmed.split(",") + // ["👍":1]["👎":1]["😃":1]["😯":1] + for (mapStr in mapList) { + val emojiMapList = mapStr.split(":") + val emoji = emojiMapList[0].replace("\"", "") // removes double quotes + val count = emojiMapList[1].toInt() + map[emoji] = count + } + // [👍:1],[👎:1],[😃:1],[😯:1] + return map + } catch (e: IOException) { + Log.e("LinkedHashMapConverter", "Error parsing string: $value to linkedHashMap $e") + linkedMapOf() + } + } + + @TypeConverter + fun linkedHashMapToString(map: LinkedHashMap?): String = + try { + val stringWriter = java.io.StringWriter() + jsonFactory.createGenerator(stringWriter).use { generator -> + converter.serialize(map ?: linkedMapOf(), null, false, generator) + } + stringWriter.toString() + } catch (e: IOException) { + // e.printStackTrace() + "" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapStringIntConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapStringIntConverter.kt new file mode 100644 index 0000000..feed96c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/LinkedHashMapStringIntConverter.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.source.local.converters + +import com.bluelinelabs.logansquare.typeconverters.TypeConverter +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonGenerator +import java.io.IOException + +class LinkedHashMapStringIntConverter : TypeConverter> { + + @Throws(IOException::class) + override fun parse(jsonParser: JsonParser?): LinkedHashMap { + val map: LinkedHashMap = linkedMapOf() + jsonParser?.apply { + while (nextToken() != null) { + val key = text + nextToken() + val value = intValue + map[key] = value + } + } + return map + } + + @Throws(IOException::class) + override fun serialize( + `object`: LinkedHashMap?, + fieldName: String?, + writeFieldNameForObject: Boolean, + jsonGenerator: JsonGenerator? + ) { + jsonGenerator?.apply { + if (fieldName != null) { + writeFieldName(fieldName) + } + writeStartObject() + `object`?.forEach { (key, value) -> + writeFieldName(key) + writeNumber(value) + } + writeEndObject() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/PushConfigurationConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/PushConfigurationConverter.kt new file mode 100644 index 0000000..8b60082 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/PushConfigurationConverter.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.models.json.push.PushConfigurationState + +class PushConfigurationConverter { + + @TypeConverter + fun fromPushConfigurationToString(pushConfiguration: PushConfigurationState?): String = + if (pushConfiguration == null) { + "" + } else { + LoganSquare.serialize(pushConfiguration) + } + + @TypeConverter + fun fromStringToPushConfiguration(value: String?): PushConfigurationState? { + return if (value.isNullOrBlank()) { + null + } else { + return LoganSquare.parse(value, PushConfigurationState::class.java) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SendStatusConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SendStatusConverter.kt new file mode 100644 index 0000000..2a1fbb1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SendStatusConverter.kt @@ -0,0 +1,19 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.nextcloud.talk.data.database.model.SendStatus + +class SendStatusConverter { + @TypeConverter + fun fromStatus(value: SendStatus): String = value.name + + @TypeConverter + fun toStatus(value: String): SendStatus = SendStatus.valueOf(value) +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ServerVersionConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ServerVersionConverter.kt new file mode 100644 index 0000000..517dd28 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ServerVersionConverter.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.models.json.capabilities.ServerVersion + +class ServerVersionConverter { + @TypeConverter + fun fromServerVersionToString(serverVersion: ServerVersion?): String = + if (serverVersion == null) { + "" + } else { + LoganSquare.serialize(serverVersion) + } + + @TypeConverter + fun fromStringToServerVersion(value: String): ServerVersion? { + return if (value.isBlank()) { + null + } else { + return LoganSquare.parse(value, ServerVersion::class.java) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SignalingSettingsConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SignalingSettingsConverter.kt new file mode 100644 index 0000000..2a0ece8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SignalingSettingsConverter.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.models.json.signaling.settings.SignalingSettings + +class SignalingSettingsConverter { + + @TypeConverter + fun fromSignalingSettingsToString(signalingSettings: SignalingSettings?): String = + if (signalingSettings == null) { + "" + } else { + LoganSquare.serialize(signalingSettings) + } + + @TypeConverter + fun fromStringToSignalingSettings(value: String): SignalingSettings? { + return if (value.isBlank()) { + null + } else { + return LoganSquare.parse(value, SignalingSettings::class.java) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStorageMapper.kt b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStorageMapper.kt new file mode 100644 index 0000000..eb83364 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStorageMapper.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.storage + +import com.nextcloud.talk.data.storage.model.ArbitraryStorage +import com.nextcloud.talk.data.storage.model.ArbitraryStorageEntity + +object ArbitraryStorageMapper { + fun toModel(entity: ArbitraryStorageEntity?): ArbitraryStorage? = + entity?.let { + ArbitraryStorage( + it.accountIdentifier, + it.key, + it.storageObject, + it.value + ) + } + + fun toEntity(model: ArbitraryStorage): ArbitraryStorageEntity = + ArbitraryStorageEntity( + accountIdentifier = model.accountIdentifier, + key = model.key, + storageObject = model.storageObject, + value = model.value + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesDao.kt b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesDao.kt new file mode 100644 index 0000000..0fe66e1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesDao.kt @@ -0,0 +1,40 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.storage + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.nextcloud.talk.data.storage.model.ArbitraryStorageEntity +import io.reactivex.Maybe + +@Dao +abstract class ArbitraryStoragesDao { + @Query( + "SELECT * FROM ArbitraryStorage WHERE " + + "accountIdentifier = :accountIdentifier AND " + + "\"key\" = :key AND " + + "object = :objectString" + ) + abstract fun getStorageSetting( + accountIdentifier: Long, + key: String, + objectString: String + ): Maybe + + @Query( + "SELECT * FROM ArbitraryStorage" + ) + abstract fun getAll(): Maybe> + + @Query("DELETE FROM ArbitraryStorage WHERE accountIdentifier = :accountIdentifier") + abstract fun deleteArbitraryStorage(accountIdentifier: Long): Int + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun saveArbitraryStorage(arbitraryStorage: ArbitraryStorageEntity): Long +} diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepository.kt b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepository.kt new file mode 100644 index 0000000..3ca2b84 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepository.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.storage + +import com.nextcloud.talk.data.storage.model.ArbitraryStorage +import com.nextcloud.talk.data.storage.model.ArbitraryStorageEntity +import io.reactivex.Maybe + +interface ArbitraryStoragesRepository { + fun getStorageSetting(accountIdentifier: Long, key: String, objectString: String): Maybe + fun deleteArbitraryStorage(accountIdentifier: Long): Int + fun saveArbitraryStorage(arbitraryStorage: ArbitraryStorage): Long + fun getAll(): Maybe> +} diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepositoryImpl.kt new file mode 100644 index 0000000..7cf791f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepositoryImpl.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.storage + +import com.nextcloud.talk.data.storage.model.ArbitraryStorage +import com.nextcloud.talk.data.storage.model.ArbitraryStorageEntity +import io.reactivex.Maybe + +class ArbitraryStoragesRepositoryImpl(private val arbitraryStoragesDao: ArbitraryStoragesDao) : + ArbitraryStoragesRepository { + override fun getStorageSetting( + accountIdentifier: Long, + key: String, + objectString: String + ): Maybe = + arbitraryStoragesDao + .getStorageSetting(accountIdentifier, key, objectString) + .map { ArbitraryStorageMapper.toModel(it) } + + override fun getAll(): Maybe> = arbitraryStoragesDao.getAll() + + override fun deleteArbitraryStorage(accountIdentifier: Long): Int = + arbitraryStoragesDao.deleteArbitraryStorage(accountIdentifier) + + override fun saveArbitraryStorage(arbitraryStorage: ArbitraryStorage): Long = + arbitraryStoragesDao.saveArbitraryStorage(ArbitraryStorageMapper.toEntity(arbitraryStorage)) +} diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorage.kt b/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorage.kt new file mode 100644 index 0000000..1d5b131 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorage.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.storage.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ArbitraryStorage( + var accountIdentifier: Long, + var key: String, + var storageObject: String? = null, + var value: String? = null +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorageEntity.kt b/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorageEntity.kt new file mode 100644 index 0000000..5c5677a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorageEntity.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.storage.model + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import kotlinx.parcelize.Parcelize + +@Parcelize +@Entity(tableName = "ArbitraryStorage", primaryKeys = ["accountIdentifier", "key"]) +data class ArbitraryStorageEntity( + @ColumnInfo(name = "accountIdentifier") + var accountIdentifier: Long = 0, + + @ColumnInfo(name = "key") + var key: String = "", + + @ColumnInfo(name = "object") + var storageObject: String? = null, + + @ColumnInfo(name = "value") + var value: String? = null +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt b/app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt new file mode 100644 index 0000000..944666f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt @@ -0,0 +1,55 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.user + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.data.user.model.UserEntity + +object UserMapper { + fun toModel(entities: List?): List = + entities?.map { user: UserEntity? -> + toModel(user)!! + } ?: emptyList() + + fun toModel(entity: UserEntity?): User? = + entity?.let { + User( + entity.id, + entity.userId, + entity.username, + entity.baseUrl!!, + entity.token, + entity.displayName, + entity.pushConfigurationState, + entity.capabilities, + entity.serverVersion, + entity.clientCertificate, + entity.externalSignalingServer, + entity.current, + entity.scheduledForDeletion + ) + } + + fun toEntity(model: User): UserEntity { + val userEntity = when (val id = model.id) { + null -> UserEntity(userId = model.userId, username = model.username, baseUrl = model.baseUrl!!) + else -> UserEntity(id, model.userId, model.username, model.baseUrl!!) + } + userEntity.apply { + token = model.token + displayName = model.displayName + pushConfigurationState = model.pushConfigurationState + capabilities = model.capabilities + serverVersion = model.serverVersion + clientCertificate = model.clientCertificate + externalSignalingServer = model.externalSignalingServer + current = model.current + scheduledForDeletion = model.scheduledForDeletion + } + return userEntity + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt new file mode 100644 index 0000000..e2291f4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt @@ -0,0 +1,84 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.user + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.nextcloud.talk.data.user.model.UserEntity +import com.nextcloud.talk.models.json.push.PushConfigurationState +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single + +@Dao +@Suppress("TooManyFunctions") +abstract class UsersDao { + // get active user + @Query("SELECT * FROM User where current = 1") + abstract fun getActiveUser(): Maybe + + // get active user + @Query("SELECT * FROM User where current = 1") + abstract fun getActiveUserObservable(): Observable + + @Query("SELECT * FROM User where current = 1") + abstract fun getActiveUserSynchronously(): UserEntity? + + @Delete + abstract fun deleteUser(user: UserEntity): Int + + @Update + abstract fun updateUser(user: UserEntity): Int + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun saveUser(user: UserEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun saveUsers(vararg users: UserEntity): List + + // get all users not scheduled for deletion + @Query("SELECT * FROM User where scheduledForDeletion != 1") + abstract fun getUsers(): Single> + + @Query("SELECT * FROM User where id = :id") + abstract fun getUserWithId(id: Long): Maybe + + @Query("SELECT * FROM User where id = :id AND scheduledForDeletion != 1") + abstract fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe + + @Query("SELECT * FROM User where userId = :userId") + abstract fun getUserWithUserId(userId: String): Maybe + + @Query("SELECT * FROM User where scheduledForDeletion = 1") + abstract fun getUsersScheduledForDeletion(): Single> + + @Query("SELECT * FROM User where scheduledForDeletion = 0") + abstract fun getUsersNotScheduledForDeletion(): Single> + + @Query("SELECT * FROM User WHERE username = :username AND baseUrl = :server") + abstract fun getUserWithUsernameAndServer(username: String, server: String): Maybe + + @Query( + "UPDATE User SET current = CASE " + + "WHEN id == :id THEN 1 " + + "WHEN id != :id THEN 0 " + + "END" + ) + abstract fun setUserAsActiveWithId(id: Long): Int + + @Query("Update User SET pushConfigurationState = :state WHERE id == :id") + abstract fun updatePushState(id: Long, state: PushConfigurationState): Single + + companion object { + const val TAG = "UsersDao" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt new file mode 100644 index 0000000..4889b5c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.user + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.push.PushConfigurationState +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single + +@Suppress("TooManyFunctions") +interface UsersRepository { + fun getActiveUser(): Maybe + fun getActiveUserObservable(): Observable + fun getUsers(): Single> + fun getUserWithId(id: Long): Maybe + fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe + fun getUserWithUserId(userId: String): Maybe + fun getUsersScheduledForDeletion(): Single> + fun getUsersNotScheduledForDeletion(): Single> + fun getUserWithUsernameAndServer(username: String, server: String): Maybe + fun updateUser(user: User): Int + fun insertUser(user: User): Long + fun setUserAsActiveWithId(id: Long): Single + fun deleteUser(user: User): Int + fun updatePushState(id: Long, state: PushConfigurationState): Single +} diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt new file mode 100644 index 0000000..e3a835f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt @@ -0,0 +1,85 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.user + +import android.util.Log +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.push.PushConfigurationState +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single + +@Suppress("TooManyFunctions") +class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository { + + override fun getActiveUser(): Maybe { + val user = usersDao.getActiveUser() + .map { + setUserAsActiveWithId(it.id) + UserMapper.toModel(it)!! + } + return user + } + + override fun getActiveUserObservable(): Observable = + usersDao.getActiveUserObservable().map { + UserMapper.toModel(it) + } + + override fun getUsers(): Single> = usersDao.getUsers().map { UserMapper.toModel(it) } + + override fun getUserWithId(id: Long): Maybe = usersDao.getUserWithId(id).map { UserMapper.toModel(it) } + + override fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe = + usersDao.getUserWithIdNotScheduledForDeletion(id).map { + UserMapper.toModel(it) + } + + override fun getUserWithUserId(userId: String): Maybe = + usersDao.getUserWithUserId(userId).map { + UserMapper.toModel(it) + } + + override fun getUsersScheduledForDeletion(): Single> = + usersDao.getUsersScheduledForDeletion().map { + UserMapper.toModel(it) + } + + override fun getUsersNotScheduledForDeletion(): Single> = + usersDao.getUsersNotScheduledForDeletion().map { + UserMapper.toModel(it) + } + + override fun getUserWithUsernameAndServer(username: String, server: String): Maybe = + usersDao.getUserWithUsernameAndServer(username, server).map { + UserMapper.toModel(it) + } + + override fun updateUser(user: User): Int = usersDao.updateUser(UserMapper.toEntity(user)) + + override fun insertUser(user: User): Long = usersDao.saveUser(UserMapper.toEntity(user)) + + override fun setUserAsActiveWithId(id: Long): Single { + val amountUpdated = usersDao.setUserAsActiveWithId(id) + Log.d(TAG, "setUserAsActiveWithId. amountUpdated: $amountUpdated") + return if (amountUpdated > 0) { + Single.just(true) + } else { + Single.just(false) + } + } + + override fun deleteUser(user: User): Int = usersDao.deleteUser(UserMapper.toEntity(user)) + + override fun updatePushState(id: Long, state: PushConfigurationState): Single = + usersDao.updatePushState(id, state) + + companion object { + private val TAG = UsersRepositoryImpl::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/user/model/User.kt b/app/src/main/java/com/nextcloud/talk/data/user/model/User.kt new file mode 100644 index 0000000..a94ec01 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/model/User.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.user.model + +import android.os.Parcelable +import android.util.Log +import com.nextcloud.talk.models.ExternalSignalingServer +import com.nextcloud.talk.models.json.capabilities.Capabilities +import com.nextcloud.talk.models.json.capabilities.ServerVersion +import com.nextcloud.talk.models.json.push.PushConfigurationState +import com.nextcloud.talk.utils.ApiUtils +import kotlinx.parcelize.Parcelize +import java.lang.Boolean.FALSE + +@Parcelize +data class User( + var id: Long? = null, + var userId: String? = null, + var username: String? = null, + var baseUrl: String? = null, + var token: String? = null, + var displayName: String? = null, + var pushConfigurationState: PushConfigurationState? = null, + var capabilities: Capabilities? = null, + var serverVersion: ServerVersion? = null, + var clientCertificate: String? = null, + var externalSignalingServer: ExternalSignalingServer? = null, + var current: Boolean = FALSE, + var scheduledForDeletion: Boolean = FALSE +) : Parcelable { + + fun getCredentials(): String = ApiUtils.getCredentials(username, token)!! + + fun hasSpreedFeatureCapability(capabilityName: String): Boolean { + if (capabilities == null) { + Log.e(TAG, "Capabilities are null in hasSpreedFeatureCapability. false is returned for capability check") + } + return capabilities?.spreedCapability?.features?.contains(capabilityName) ?: false + } + + companion object { + private val TAG = User::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt b/app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt new file mode 100644 index 0000000..66fdf16 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.data.user.model + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.nextcloud.talk.models.ExternalSignalingServer +import com.nextcloud.talk.models.json.capabilities.Capabilities +import com.nextcloud.talk.models.json.capabilities.ServerVersion +import com.nextcloud.talk.models.json.push.PushConfigurationState +import kotlinx.parcelize.Parcelize +import java.lang.Boolean.FALSE + +@Parcelize +@Entity(tableName = "User") +data class UserEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + var id: Long = 0, + + @ColumnInfo(name = "userId") + var userId: String? = null, + + @ColumnInfo(name = "username") + var username: String? = null, + + @ColumnInfo(name = "baseUrl") + var baseUrl: String? = null, + + @ColumnInfo(name = "token") + var token: String? = null, + + @ColumnInfo(name = "displayName") + var displayName: String? = null, + + @ColumnInfo(name = "pushConfigurationState") + var pushConfigurationState: PushConfigurationState? = null, + + @ColumnInfo(name = "capabilities") + var capabilities: Capabilities? = null, + + @ColumnInfo(name = "serverVersion", defaultValue = "") + var serverVersion: ServerVersion? = null, + + @ColumnInfo(name = "clientCertificate") + var clientCertificate: String? = null, + + @ColumnInfo(name = "externalSignalingServer") + var externalSignalingServer: ExternalSignalingServer? = null, + + @ColumnInfo(name = "current") + var current: Boolean = FALSE, + + @ColumnInfo(name = "scheduledForDeletion") + var scheduledForDeletion: Boolean = FALSE +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt new file mode 100644 index 0000000..67a21c0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseActivity.kt @@ -0,0 +1,496 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.diagnose + +import android.annotation.SuppressLint +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Intent +import android.os.Build +import android.os.Build.MANUFACTURER +import android.os.Build.MODEL +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.widget.Toast +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StandardAppBar +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.BrandingUtils +import com.nextcloud.talk.utils.ClosedInterfaceImpl +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.PushUtils.Companion.LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY +import com.nextcloud.talk.utils.PushUtils.Companion.LATEST_PUSH_REGISTRATION_AT_SERVER +import com.nextcloud.talk.utils.UserIdUtils +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil +import com.nextcloud.talk.utils.power.PowerManagerUtils +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +@Suppress("TooManyFunctions") +class DiagnoseActivity : BaseActivity() { + + @Inject + lateinit var arbitraryStorageManager: ArbitraryStorageManager + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var platformPermissionUtil: PlatformPermissionUtil + + private var isGooglePlayServicesAvailable: Boolean = false + + sealed class DiagnoseElement { + data class DiagnoseHeadline(val headline: String) : DiagnoseElement() + data class DiagnoseEntry(val key: String, val value: String) : DiagnoseElement() + } + + private val diagnoseData = mutableListOf() + private val diagnoseDataState = mutableStateOf(emptyList()) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + val diagnoseViewModel = ViewModelProvider( + this, + viewModelFactory + )[DiagnoseViewModel::class.java] + + val colorScheme = viewThemeUtils.getColorScheme(this) + isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable + + setContent { + val backgroundColor = colorResource(id = R.color.bg_default) + + val menuItems = mutableListOf( + stringResource(R.string.nc_common_copy) to { copyToClipboard(diagnoseData.toMarkdownString()) }, + stringResource(R.string.share) to { shareToOtherApps(diagnoseData.toMarkdownString()) }, + stringResource(R.string.send_email) to { composeEmail(diagnoseData.toMarkdownString()) } + ) + + if (BrandingUtils.isOriginalNextcloudClient(applicationContext)) { + menuItems.add( + stringResource(R.string.create_issue) to { createGithubIssue(diagnoseData.toMarkdownString()) } + ) + } + + MaterialTheme( + colorScheme = colorScheme + ) { + ColoredStatusBar() + Scaffold( + modifier = Modifier + .statusBarsPadding() + .displayCutoutPadding(), + topBar = { + StandardAppBar( + title = stringResource(R.string.nc_settings_diagnose_title), + menuItems + ) + }, + content = { paddingValues -> + val viewState = diagnoseViewModel.notificationViewState.collectAsState().value + + Column( + Modifier + .background(backgroundColor) + .padding( + 0.dp, + paddingValues.calculateTopPadding(), + 0.dp, + paddingValues.calculateBottomPadding() + ) + .fillMaxSize() + ) { + DiagnoseContentComposable( + diagnoseDataState, + isLoading = diagnoseViewModel.isLoading.value, + showDialog = diagnoseViewModel.showDialog.value, + viewState = viewState, + onTestPushClick = { diagnoseViewModel.fetchTestPushResult() }, + onDismissDialog = { diagnoseViewModel.dismissDialog() }, + isGooglePlayServicesAvailable = isGooglePlayServicesAvailable + ) + } + } + ) + } + } + } + + override fun onResume() { + super.onResume() + supportActionBar?.show() + + diagnoseData.clear() + setupMetaValues() + setupPhoneValues() + setupAppValues() + setupAccountValues() + + diagnoseDataState.value = diagnoseData.toList() + } + + private fun shareToOtherApps(message: String) { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, message) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, getString(R.string.share)) + startActivity(shareIntent) + } + + private fun composeEmail(text: String) { + val intent = Intent(Intent.ACTION_SENDTO).apply { + val appName = context.resources.getString(R.string.nc_app_product_name) + + data = "mailto:".toUri() + putExtra(Intent.EXTRA_SUBJECT, appName) + putExtra(Intent.EXTRA_TEXT, text) + } + if (intent.resolveActivity(packageManager) != null) { + startActivity(intent) + } + } + + private fun createGithubIssue(text: String) { + copyToClipboard(text) + + startActivity( + Intent( + Intent.ACTION_VIEW, + resources!!.getString(R.string.nc_talk_android_issues_url).toUri() + ) + ) + } + + private fun copyToClipboard(text: String) { + val clipboardManager = + getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText( + resources?.getString(R.string.nc_app_product_name), + text + ) + clipboardManager.setPrimaryClip(clipData) + + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_copy_success), + Toast.LENGTH_LONG + ).show() + } + + private fun setupMetaValues() { + addHeadline(context.resources.getString(R.string.nc_diagnose_meta_category_title)) + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_meta_system_report_date), + value = DisplayUtils.unixTimeToHumanReadable(System.currentTimeMillis()) + ) + } + + private fun setupPhoneValues() { + addHeadline(context.resources.getString(R.string.nc_diagnose_phone_category_title)) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_device_name_title), + value = getDeviceName() + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_android_version_title), + value = Build.VERSION.SDK_INT.toString() + ) + + if (isGooglePlayServicesAvailable) { + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_gplay_available_title), + value = context.resources.getString(R.string.nc_diagnose_gplay_available_yes) + ) + } else { + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_gplay_available_title), + value = context.resources.getString(R.string.nc_diagnose_gplay_available_no) + ) + } + } + + @SuppressLint("SetTextI18n") + @Suppress("MagicNumber") + private fun setupAppValues() { + addHeadline(context.resources.getString(R.string.nc_diagnose_app_category_title)) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_app_name_title), + value = context.resources.getString(R.string.nc_app_product_name) + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_app_version_title), + value = String.format("v" + BuildConfig.VERSION_NAME) + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_flavor), + value = BuildConfig.FLAVOR + ) + + if (isGooglePlayServicesAvailable) { + setupAppValuesForGooglePlayServices() + } + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_app_users_amount), + value = userManager.users.blockingGet().size.toString() + ) + } + + @Suppress("Detekt.LongMethod") + private fun setupAppValuesForGooglePlayServices() { + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_battery_optimization_title), + value = if (PowerManagerUtils().isIgnoringBatteryOptimizations()) { + context.resources.getString(R.string.nc_diagnose_battery_optimization_ignored) + } else { + context.resources.getString(R.string.nc_diagnose_battery_optimization_not_ignored) + } + ) + + // handle notification permission on API level >= 33 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_notification_permission), + value = if (platformPermissionUtil.isPostNotificationsPermissionGranted()) { + context.resources.getString(R.string.nc_settings_notifications_granted) + } else { + context.resources.getString(R.string.nc_settings_notifications_declined) + } + ) + } + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_notification_calls_channel_permission), + value = + translateBoolean( + NotificationUtils.isCallsNotificationChannelEnabled(this) + ) + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_notification_messages_channel_permission), + value = + translateBoolean( + NotificationUtils.isMessagesNotificationChannelEnabled(this) + ) + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_firebase_push_token_title), + value = if (appPreferences.pushToken.isNullOrEmpty()) { + context.resources.getString(R.string.nc_diagnose_firebase_push_token_missing) + } else { + "${appPreferences.pushToken.substring(0, PUSH_TOKEN_PREFIX_END)}..." + } + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_firebase_push_token_latest_generated), + value = if (appPreferences.pushTokenLatestGeneration != null && + appPreferences.pushTokenLatestGeneration != 0L + ) { + DisplayUtils.unixTimeToHumanReadable( + appPreferences + .pushTokenLatestGeneration + ) + } else { + context.resources.getString(R.string.nc_common_unknown) + } + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_firebase_push_token_latest_fetch), + value = if (appPreferences.pushTokenLatestFetch != null && appPreferences.pushTokenLatestFetch != 0L) { + DisplayUtils.unixTimeToHumanReadable(appPreferences.pushTokenLatestFetch) + } else { + context.resources.getString(R.string.nc_common_unknown) + } + ) + } + + private fun setupAccountValues() { + val currentUser = currentUserProvider.currentUser.blockingGet() + + addHeadline(context.resources.getString(R.string.nc_diagnose_account_category_title)) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_account_server), + value = + currentUser.baseUrl!! + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_account_user_name), + value = + currentUser.displayName!! + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_account_user_status_enabled), + value = + translateBoolean( + (currentUser.capabilities?.userStatusCapability?.enabled) + ) + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_account_server_notification_app), + value = + translateBoolean(currentUser.capabilities?.notificationsCapability?.features?.isNotEmpty()) + ) + + if (isGooglePlayServicesAvailable) { + setupPushRegistrationDiagnose() + } + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_server_version), + value = + currentUser.serverVersion?.versionString!! + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_server_talk_version), + value = + currentUser.capabilities?.spreedCapability?.version!! + ) + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_signaling_mode_title), + value = + if (currentUser.externalSignalingServer?.externalSignalingServer?.isNotEmpty() == true) { + context.resources.getString(R.string.nc_diagnose_signaling_mode_extern) + } else { + context.resources.getString(R.string.nc_diagnose_signaling_mode_intern) + } + ) + } + + private fun setupPushRegistrationDiagnose() { + val accountId = UserIdUtils.getIdForUser(currentUserProvider.currentUser.blockingGet()) + + val latestPushRegistrationAtServer = arbitraryStorageManager.getStorageSetting( + accountId, + LATEST_PUSH_REGISTRATION_AT_SERVER, + "" + ).blockingGet()?.value + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_latest_push_registration_at_server), + if (latestPushRegistrationAtServer.isNullOrEmpty()) { + context.resources.getString(R.string.nc_diagnose_latest_push_registration_at_server_fail) + } else { + DisplayUtils.unixTimeToHumanReadable(latestPushRegistrationAtServer.toLong()) + } + ) + + val latestPushRegistrationAtPushProxy = arbitraryStorageManager.getStorageSetting( + accountId, + LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY, + "" + ).blockingGet()?.value + + addDiagnosisEntry( + key = context.resources.getString(R.string.nc_diagnose_latest_push_registration_at_push_proxy), + value = if (latestPushRegistrationAtPushProxy.isNullOrEmpty()) { + context.resources.getString(R.string.nc_diagnose_latest_push_registration_at_push_proxy_fail) + } else { + DisplayUtils.unixTimeToHumanReadable(latestPushRegistrationAtPushProxy.toLong()) + } + ) + } + + private fun getDeviceName(): String = + if (MODEL.startsWith(MANUFACTURER, ignoreCase = true)) { + MODEL + } else { + "$MANUFACTURER $MODEL" + } + + private fun translateBoolean(answer: Boolean?): String = + when (answer) { + null -> context.resources.getString(R.string.nc_common_unknown) + true -> context.resources.getString(R.string.nc_yes) + else -> context.resources.getString(R.string.nc_no) + } + + private fun List.toMarkdownString(): String { + val markdownText = SpannableStringBuilder() + + this.forEach { + when (it) { + is DiagnoseElement.DiagnoseHeadline -> { + markdownText.append("$MARKDOWN_HEADLINE ${it.headline}") + markdownText.append("\n\n") + } + + is DiagnoseElement.DiagnoseEntry -> { + markdownText.append("$MARKDOWN_BOLD${it.key}$MARKDOWN_BOLD") + markdownText.append("\n\n") + markdownText.append(it.value) + markdownText.append("\n\n") + } + } + } + return markdownText.toString() + } + + private fun addHeadline(text: String) { + diagnoseData.add(DiagnoseElement.DiagnoseHeadline(text)) + } + + private fun addDiagnosisEntry(key: String, value: String) { + diagnoseData.add(DiagnoseElement.DiagnoseEntry(key, value)) + } + + companion object { + val TAG = DiagnoseActivity::class.java.simpleName + private const val MARKDOWN_HEADLINE = "###" + private const val MARKDOWN_BOLD = "**" + private const val PUSH_TOKEN_PREFIX_END: Int = 5 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseContentComposable.kt b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseContentComposable.kt new file mode 100644 index 0000000..1db7c12 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseContentComposable.kt @@ -0,0 +1,264 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.diagnose + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.nextcloud.talk.R + +@Suppress("LongParameterList") +@Composable +fun DiagnoseContentComposable( + data: State>, + isLoading: Boolean, + showDialog: Boolean, + viewState: NotificationUiState, + onTestPushClick: () -> Unit, + onDismissDialog: () -> Unit, + isGooglePlayServicesAvailable: Boolean +) { + val context = LocalContext.current + Column( + modifier = Modifier + .fillMaxSize() + .padding( + start = 16.dp, + top = 0.dp, + end = 16.dp, + bottom = 0.dp + ) + .verticalScroll(rememberScrollState()) + ) { + data.value.forEach { element -> + when (element) { + is DiagnoseActivity.DiagnoseElement.DiagnoseHeadline -> { + Text( + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + text = element.headline, + color = MaterialTheme.colorScheme.primary, + fontSize = LocalDensity.current.run { + dimensionResource(R.dimen.headline_text_size).toPx().toSp() + }, + fontWeight = FontWeight.Bold + ) + } + + is DiagnoseActivity.DiagnoseElement.DiagnoseEntry -> { + Text( + text = element.key, + color = colorResource(R.color.high_emphasis_text), + fontWeight = FontWeight.Bold + ) + Text( + modifier = Modifier.padding(bottom = 16.dp), + text = element.value, + color = colorResource(R.color.high_emphasis_text) + ) + } + } + } + if (isGooglePlayServicesAvailable) { + ShowTestPushButton(onTestPushClick) + } + ShowNotificationData(isLoading, showDialog, context, viewState, onDismissDialog) + Spacer(modifier = Modifier.height(40.dp)) + } +} + +@Composable +fun ShowTestPushButton(onTestPushClick: () -> Unit) { + Button( + modifier = Modifier + .wrapContentSize() + .padding(vertical = 8.dp), + onClick = { + onTestPushClick() + } + ) { + Text( + text = stringResource(R.string.nc_test_push_button), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + fontSize = LocalDensity.current.run { + dimensionResource(R.dimen.headline_text_size).toPx().toSp() + }, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + } +} + +@Composable +fun ShowOptions(onDismissDialog: () -> Unit, message: String, context: Context) { + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier.fillMaxWidth() + ) { + TextButton(onClick = { onDismissDialog() }) { + Text(text = stringResource(R.string.nc_cancel)) + } + Spacer(modifier = Modifier.width(8.dp)) + TextButton(onClick = { + val clipboard = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Push Message", message) + clipboard.setPrimaryClip(clip) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(context, R.string.message_copied, Toast.LENGTH_SHORT).show() + } + onDismissDialog() + }) { + Text(text = stringResource(R.string.nc_common_copy)) + } + } +} + +@Composable +fun LoadingIndicator() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} + +@Composable +fun ShowNotificationData( + isLoading: Boolean, + showDialog: Boolean, + context: Context, + viewState: NotificationUiState, + onDismissDialog: () -> Unit +) { + val message = getMessage(context, viewState) + + if (isLoading) { + LoadingIndicator() + } + if (showDialog) { + Dialog( + onDismissRequest = { onDismissDialog() }, + properties = DialogProperties( + dismissOnClickOutside = true, + usePlatformDefaultWidth = false + ) + ) { + Surface( + shape = MaterialTheme.shapes.medium, + tonalElevation = 8.dp, + modifier = Modifier + .wrapContentSize() + .padding(16.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(R.string.nc_test_results), + style = MaterialTheme.typography + .titleMedium + ) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = false) + .verticalScroll(rememberScrollState()) + ) { + Column(modifier = Modifier.padding(top = 12.dp)) { + if (viewState is NotificationUiState.Success) { + Text( + text = stringResource(R.string.nc_push_notification_message), + color = colorResource(R.color.colorPrimary) + ) + } + Text( + modifier = Modifier.padding(top = 12.dp), + text = message + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + ShowOptions(onDismissDialog, message, context) + } + } + } + } +} + +fun getMessage(context: Context, viewState: NotificationUiState): String = + when (viewState) { + is NotificationUiState.Success -> + viewState.testNotification ?: context.getString(R.string.nc_push_notification_fetch_error) + + is NotificationUiState.Error -> + context.getString(R.string.nc_push_notification_error, viewState.message) + + else -> + context.getString(R.string.nc_common_error_sorry) + } + +@Preview(showBackground = true) +@Composable +fun DiagnoseContentPreview() { + val state = remember { + mutableStateOf( + listOf( + DiagnoseActivity.DiagnoseElement.DiagnoseHeadline("Headline"), + DiagnoseActivity.DiagnoseElement.DiagnoseEntry("Key", "Value") + ) + ) + } + DiagnoseContentComposable( + state, + false, + true, + NotificationUiState.Success("Test notification successful"), + {}, + {}, + true + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseViewModel.kt b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseViewModel.kt new file mode 100644 index 0000000..6fca3b8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/diagnose/DiagnoseViewModel.kt @@ -0,0 +1,69 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.diagnose + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("TooGenericExceptionCaught") +class DiagnoseViewModel @Inject constructor( + private val ncApiCoroutines: NcApiCoroutines, + private val currentUserProvider: CurrentUserProviderNew +) : ViewModel() { + private val _currentUser = currentUserProvider.currentUser.blockingGet() + val currentUser: User = _currentUser + val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: "" + + private val _notificationViewState = MutableStateFlow(NotificationUiState.None) + val notificationViewState: StateFlow = _notificationViewState + + private val _isLoading = mutableStateOf(false) + val isLoading = _isLoading + + private val _showDialog = mutableStateOf(false) + val showDialog = _showDialog + + fun fetchTestPushResult() { + viewModelScope.launch { + try { + _isLoading.value = true + val response = ncApiCoroutines.testPushNotifications( + credentials, + ApiUtils + .getUrlForTestPushNotifications(_currentUser.baseUrl ?: "") + ) + val notificationMessage = response.ocs?.data?.message + _notificationViewState.value = NotificationUiState.Success(notificationMessage) + } catch (e: Exception) { + _notificationViewState.value = NotificationUiState.Error(e.message ?: "") + } finally { + _isLoading.value = false + _showDialog.value = true + } + } + } + + fun dismissDialog() { + _showDialog.value = false + } +} + +sealed class NotificationUiState { + data object None : NotificationUiState() + data class Success(val testNotification: String?) : NotificationUiState() + data class Error(val message: String) : NotificationUiState() +} diff --git a/app/src/main/java/com/nextcloud/talk/events/CertificateEvent.java b/app/src/main/java/com/nextcloud/talk/events/CertificateEvent.java new file mode 100644 index 0000000..3e5954d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/CertificateEvent.java @@ -0,0 +1,42 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.events; + +import android.webkit.SslErrorHandler; + +import com.nextcloud.talk.utils.ssl.TrustManager; + +import java.security.cert.X509Certificate; + +import androidx.annotation.Nullable; + +public class CertificateEvent { + private final X509Certificate x509Certificate; + private final TrustManager trustManager; + @Nullable + private final SslErrorHandler sslErrorHandler; + + public CertificateEvent(X509Certificate x509Certificate, TrustManager trustManager, + @Nullable SslErrorHandler sslErrorHandler) { + this.x509Certificate = x509Certificate; + this.trustManager = trustManager; + this.sslErrorHandler = sslErrorHandler; + } + + @Nullable + public SslErrorHandler getSslErrorHandler() { + return sslErrorHandler; + } + + public X509Certificate getX509Certificate() { + return x509Certificate; + } + + public TrustManager getTrustManager() { + return trustManager; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/events/ConfigurationChangeEvent.java b/app/src/main/java/com/nextcloud/talk/events/ConfigurationChangeEvent.java new file mode 100644 index 0000000..20f2cf4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/ConfigurationChangeEvent.java @@ -0,0 +1,34 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.events; + +public class ConfigurationChangeEvent { + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ConfigurationChangeEvent)) { + return false; + } + final ConfigurationChangeEvent other = (ConfigurationChangeEvent) o; + + return other.canEqual((Object) this); + } + + protected boolean canEqual(final Object other) { + return other instanceof ConfigurationChangeEvent; + } + + public int hashCode() { + return 1; + } + + public String toString() { + return "ConfigurationChangeEvent()"; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/events/ConversationsListFetchDataEvent.kt b/app/src/main/java/com/nextcloud/talk/events/ConversationsListFetchDataEvent.kt new file mode 100644 index 0000000..f1172e2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/ConversationsListFetchDataEvent.kt @@ -0,0 +1,9 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.events + +class ConversationsListFetchDataEvent diff --git a/app/src/main/java/com/nextcloud/talk/events/EventStatus.java b/app/src/main/java/com/nextcloud/talk/events/EventStatus.java new file mode 100644 index 0000000..c847090 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/EventStatus.java @@ -0,0 +1,90 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.events; + +public class EventStatus { + private long userId; + private EventType eventType; + private boolean allGood; + + public EventStatus(long userId, EventType eventType, boolean allGood) { + this.userId = userId; + this.eventType = eventType; + this.allGood = allGood; + } + + public long getUserId() { + return this.userId; + } + + public EventType getEventType() { + return this.eventType; + } + + public boolean isAllGood() { + return this.allGood; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + public void setEventType(EventType eventType) { + this.eventType = eventType; + } + + public void setAllGood(boolean allGood) { + this.allGood = allGood; + } + + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof EventStatus)) { + return false; + } + final EventStatus other = (EventStatus) o; + if (!other.canEqual((Object) this)) { + return false; + } + if (this.getUserId() != other.getUserId()) { + return false; + } + final Object this$eventType = this.getEventType(); + final Object other$eventType = other.getEventType(); + if (this$eventType == null ? other$eventType != null : !this$eventType.equals(other$eventType)) { + return false; + } + + return this.isAllGood() == other.isAllGood(); + } + + protected boolean canEqual(final Object other) { + return other instanceof EventStatus; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final long $userId = this.getUserId(); + result = result * PRIME + (int) ($userId >>> 32 ^ $userId); + final Object $eventType = this.getEventType(); + result = result * PRIME + ($eventType == null ? 43 : $eventType.hashCode()); + return result * PRIME + (this.isAllGood() ? 79 : 97); + } + + public String toString() { + return "EventStatus(userId=" + this.getUserId() + ", eventType=" + this.getEventType() + ", allGood=" + this.isAllGood() + ")"; + } + + public enum EventType { + PUSH_REGISTRATION, CAPABILITIES_FETCH, SIGNALING_SETTINGS, CONVERSATION_UPDATE, PARTICIPANTS_UPDATE + } + +} diff --git a/app/src/main/java/com/nextcloud/talk/events/MoreMenuClickEvent.java b/app/src/main/java/com/nextcloud/talk/events/MoreMenuClickEvent.java new file mode 100644 index 0000000..dab4200 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/MoreMenuClickEvent.java @@ -0,0 +1,54 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.events; + +import com.nextcloud.talk.models.json.conversations.Conversation; + +public class MoreMenuClickEvent { + private final Conversation conversation; + + public MoreMenuClickEvent(Conversation conversation) { + this.conversation = conversation; + } + + public Conversation getConversation() { + return this.conversation; + } + + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof MoreMenuClickEvent)) { + return false; + } + final MoreMenuClickEvent other = (MoreMenuClickEvent) o; + if (!other.canEqual((Object) this)) { + return false; + } + final Object this$conversation = this.getConversation(); + final Object other$conversation = other.getConversation(); + + return this$conversation == null ? other$conversation == null : this$conversation.equals(other$conversation); + } + + protected boolean canEqual(final Object other) { + return other instanceof MoreMenuClickEvent; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $conversation = this.getConversation(); + return result * PRIME + ($conversation == null ? 43 : $conversation.hashCode()); + } + + public String toString() { + return "MoreMenuClickEvent(conversation=" + this.getConversation() + ")"; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/events/NetworkEvent.java b/app/src/main/java/com/nextcloud/talk/events/NetworkEvent.java new file mode 100644 index 0000000..ec90149 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/NetworkEvent.java @@ -0,0 +1,55 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.events; + +public class NetworkEvent { + private final NetworkConnectionEvent networkConnectionEvent; + + public NetworkEvent(NetworkConnectionEvent networkConnectionEvent) { + this.networkConnectionEvent = networkConnectionEvent; + } + + public NetworkConnectionEvent getNetworkConnectionEvent() { + return this.networkConnectionEvent; + } + + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof NetworkEvent)) { + return false; + } + final NetworkEvent other = (NetworkEvent) o; + if (!other.canEqual((Object) this)) { + return false; + } + final Object this$networkConnectionEvent = this.getNetworkConnectionEvent(); + final Object other$networkConnectionEvent = other.getNetworkConnectionEvent(); + + return this$networkConnectionEvent == null ? other$networkConnectionEvent == null : this$networkConnectionEvent.equals(other$networkConnectionEvent); + } + + protected boolean canEqual(final Object other) { + return other instanceof NetworkEvent; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $networkConnectionEvent = this.getNetworkConnectionEvent(); + return result * PRIME + ($networkConnectionEvent == null ? 43 : $networkConnectionEvent.hashCode()); + } + + public String toString() { + return "NetworkEvent(networkConnectionEvent=" + this.getNetworkConnectionEvent() + ")"; + } + + public enum NetworkConnectionEvent { + NETWORK_CONNECTED, NETWORK_DISCONNECTED + } +} diff --git a/app/src/main/java/com/nextcloud/talk/events/OpenConversationEvent.kt b/app/src/main/java/com/nextcloud/talk/events/OpenConversationEvent.kt new file mode 100644 index 0000000..1062e61 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/OpenConversationEvent.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.events + +import android.os.Bundle +import com.nextcloud.talk.models.json.conversations.Conversation + +class OpenConversationEvent { + var conversation: Conversation? = null + var bundle: Bundle? = null + + constructor(conversation: Conversation?, bundle: Bundle?) { + this.conversation = conversation + this.bundle = bundle + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpenConversationEvent + + if (conversation != other.conversation) return false + if (bundle != other.bundle) return false + + return true + } + + override fun hashCode(): Int { + var result = conversation?.hashCode() ?: 0 + result = 31 * result + (bundle?.hashCode() ?: 0) + return result + } +} diff --git a/app/src/main/java/com/nextcloud/talk/events/ProximitySensorEvent.java b/app/src/main/java/com/nextcloud/talk/events/ProximitySensorEvent.java new file mode 100644 index 0000000..e59d6a3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/ProximitySensorEvent.java @@ -0,0 +1,61 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.events; + +public class ProximitySensorEvent { + private final ProximitySensorEventType proximitySensorEventType; + + public ProximitySensorEvent(ProximitySensorEventType proximitySensorEventType) { + this.proximitySensorEventType = proximitySensorEventType; + } + + public ProximitySensorEventType getProximitySensorEventType() { + return this.proximitySensorEventType; + } + + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ProximitySensorEvent)) { + return false; + } + final ProximitySensorEvent other = (ProximitySensorEvent) o; + if (!other.canEqual((Object) this)) { + return false; + } + final Object this$proximitySensorEventType = this.getProximitySensorEventType(); + final Object other$proximitySensorEventType = other.getProximitySensorEventType(); + if (this$proximitySensorEventType == null ? other$proximitySensorEventType != null : !this$proximitySensorEventType.equals(other$proximitySensorEventType)) { + return false; + } + + return true; + } + + protected boolean canEqual(final Object other) { + return other instanceof ProximitySensorEvent; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $proximitySensorEventType = this.getProximitySensorEventType(); + result = result * PRIME + ($proximitySensorEventType == null ? 43 : $proximitySensorEventType.hashCode()); + return result; + } + + public String toString() { + return "ProximitySensorEvent(proximitySensorEventType=" + this.getProximitySensorEventType() + ")"; + } + + public enum ProximitySensorEventType { + SENSOR_FAR, SENSOR_NEAR + } +} diff --git a/app/src/main/java/com/nextcloud/talk/events/UserMentionClickEvent.java b/app/src/main/java/com/nextcloud/talk/events/UserMentionClickEvent.java new file mode 100644 index 0000000..7115d81 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/UserMentionClickEvent.java @@ -0,0 +1,52 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.events; + +public class UserMentionClickEvent { + public final String userId; + + public UserMentionClickEvent(String userId) { + this.userId = userId; + } + + public String getUserId() { + return this.userId; + } + + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof UserMentionClickEvent)) { + return false; + } + final UserMentionClickEvent other = (UserMentionClickEvent) o; + if (!other.canEqual((Object) this)) { + return false; + } + final Object this$userId = this.getUserId(); + final Object other$userId = other.getUserId(); + + return this$userId == null ? other$userId == null : this$userId.equals(other$userId); + } + + protected boolean canEqual(final Object other) { + return other instanceof UserMentionClickEvent; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $userId = this.getUserId(); + return result * PRIME + ($userId == null ? 43 : $userId.hashCode()); + } + + public String toString() { + return "UserMentionClickEvent(userId=" + this.getUserId() + ")"; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/events/WebSocketCommunicationEvent.java b/app/src/main/java/com/nextcloud/talk/events/WebSocketCommunicationEvent.java new file mode 100644 index 0000000..5bd5e05 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/events/WebSocketCommunicationEvent.java @@ -0,0 +1,71 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.events; + +import java.util.HashMap; + +import androidx.annotation.Nullable; + +public class WebSocketCommunicationEvent { + public final String type; + @Nullable + public final HashMap hashMap; + + public WebSocketCommunicationEvent(String type, HashMap hashMap) { + this.type = type; + this.hashMap = hashMap; + } + + public String getType() { + return this.type; + } + + @Nullable + public HashMap getHashMap() { + return this.hashMap; + } + + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof WebSocketCommunicationEvent)) { + return false; + } + final WebSocketCommunicationEvent other = (WebSocketCommunicationEvent) o; + if (!other.canEqual((Object) this)) { + return false; + } + final Object this$type = this.getType(); + final Object other$type = other.getType(); + if (this$type == null ? other$type != null : !this$type.equals(other$type)) { + return false; + } + final Object this$hashMap = this.getHashMap(); + final Object other$hashMap = other.getHashMap(); + + return this$hashMap == null ? other$hashMap == null : this$hashMap.equals(other$hashMap); + } + + protected boolean canEqual(final Object other) { + return other instanceof WebSocketCommunicationEvent; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $type = this.getType(); + result = result * PRIME + ($type == null ? 43 : $type.hashCode()); + final Object $hashMap = this.getHashMap(); + return result * PRIME + ($hashMap == null ? 43 : $hashMap.hashCode()); + } + + public String toString() { + return "WebSocketCommunicationEvent(type=" + this.getType() + ", hashMap=" + this.getHashMap() + ")"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt new file mode 100644 index 0000000..66b8584 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt @@ -0,0 +1,497 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +@file:Suppress("TooManyFunctions") + +package com.nextcloud.talk.extensions + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.util.Log +import android.widget.ImageView +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.drawable.toDrawable +import coil.annotation.ExperimentalCoilApi +import coil.imageLoader +import coil.load +import coil.request.CachePolicy +import coil.request.ImageRequest +import coil.request.SuccessResult +import coil.result +import coil.transform.CircleCropTransformation +import coil.transform.RoundedCornersTransformation +import com.nextcloud.talk.R +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.TextDrawable +import java.util.Locale +import kotlin.math.min + +private const val ROUNDING_PIXEL = 16f +private const val TAG = "ImageViewExtensions" + +@Deprecated("use other constructor that expects com.nextcloud.talk.models.domain.ConversationModel") +fun ImageView.loadConversationAvatar( + user: User, + conversation: Conversation, + ignoreCache: Boolean, + viewThemeUtils: ViewThemeUtils? +): io.reactivex.disposables.Disposable = + loadConversationAvatar( + user, + ConversationModel.mapToConversationModel(conversation, user), + ignoreCache, + viewThemeUtils + ) + +@Suppress("ReturnCount") +fun ImageView.loadConversationAvatar( + user: User, + conversation: ConversationModel, + ignoreCache: Boolean, + viewThemeUtils: ViewThemeUtils? +): io.reactivex.disposables.Disposable { + val imageRequestUri = ApiUtils.getUrlForConversationAvatarWithVersion( + 1, + user.baseUrl, + conversation.token, + DisplayUtils.isDarkModeOn(this.context), + conversation.avatarVersion + ) + + if (conversation.avatarVersion.isNullOrEmpty() && viewThemeUtils != null) { + when (conversation.type) { + ConversationEnums.ConversationType.ROOM_GROUP_CALL -> + return loadDefaultGroupCallAvatar(viewThemeUtils) + + ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> + return loadDefaultPublicCallAvatar(viewThemeUtils) + + else -> {} + } + } + + // these placeholders are only used when the request fails completely. The server also return default avatars + // when no own images are set. (although these default avatars can not be themed for the android app..) + val errorPlaceholder = + when (conversation.type) { + ConversationEnums.ConversationType.ROOM_GROUP_CALL -> + ContextCompat.getDrawable(context, R.drawable.ic_circular_group) + + ConversationEnums.ConversationType.ROOM_PUBLIC_CALL -> + ContextCompat.getDrawable(context, R.drawable.ic_circular_link) + + else -> ContextCompat.getDrawable(context, R.drawable.account_circle_96dp) + } + + return loadAvatarInternal(user, imageRequestUri, ignoreCache, errorPlaceholder) +} + +fun ImageView.loadUserAvatar( + user: User, + avatarId: String, + requestBigSize: Boolean = true, + ignoreCache: Boolean +): io.reactivex.disposables.Disposable { + val imageRequestUri = ApiUtils.getUrlForAvatar( + user.baseUrl!!, + avatarId, + requestBigSize + ) + + return loadAvatarInternal(user, imageRequestUri, ignoreCache, null) +} + +fun ImageView.loadFederatedUserAvatar(message: ChatMessage): io.reactivex.disposables.Disposable { + val cloudId = message.actorId!! + val darkTheme = if (DisplayUtils.isDarkModeOn(context)) 1 else 0 + val ignoreCache = false + val requestBigSize = true + return loadFederatedUserAvatar( + message.activeUser!!, + message.activeUser!!.baseUrl!!, + message.token!!, + cloudId, + darkTheme, + requestBigSize, + ignoreCache + ) +} + +@Suppress("LongParameterList") +fun ImageView.loadFederatedUserAvatar( + user: User, + baseUrl: String, + token: String, + cloudId: String, + darkTheme: Int, + requestBigSize: Boolean = true, + ignoreCache: Boolean +): io.reactivex.disposables.Disposable { + val imageRequestUri = ApiUtils.getUrlForFederatedAvatar( + baseUrl, + token, + cloudId, + darkTheme, + requestBigSize + ) + Log.d(TAG, "federated avatar URL: $imageRequestUri") + + return loadAvatarInternal(user, imageRequestUri, ignoreCache, null) +} + +@OptIn(ExperimentalCoilApi::class) +private fun ImageView.loadAvatarInternal( + user: User?, + url: String, + ignoreCache: Boolean, + errorPlaceholder: Drawable? +): io.reactivex.disposables.Disposable { + val cachePolicy = if (ignoreCache) { + CachePolicy.WRITE_ONLY + } else { + CachePolicy.ENABLED + } + + if (ignoreCache && this.result is SuccessResult) { + val result = this.result as SuccessResult + val memoryCacheKey = result.memoryCacheKey + val memoryCache = context.imageLoader.memoryCache + memoryCacheKey?.let { memoryCache?.remove(it) } + + val diskCacheKey = result.diskCacheKey + val diskCache = context.imageLoader.diskCache + diskCacheKey?.let { diskCache?.remove(it) } + } + + return DisposableWrapper( + load(url) { + user?.let { + addHeader( + "Authorization", + ApiUtils.getCredentials(user.username, user.token)!! + ) + } + transformations(CircleCropTransformation()) + error(errorPlaceholder ?: ContextCompat.getDrawable(context, R.drawable.account_circle_96dp)) + fallback(errorPlaceholder ?: ContextCompat.getDrawable(context, R.drawable.account_circle_96dp)) + listener(onError = { _, result -> + Log.w(TAG, "Can't load avatar with URL: $url", result.throwable) + }) + memoryCachePolicy(cachePolicy) + diskCachePolicy(cachePolicy) + } + ) +} + +@Deprecated("Use function loadAvatar", level = DeprecationLevel.WARNING) +fun ImageView.loadAvatarWithUrl(user: User? = null, url: String): io.reactivex.disposables.Disposable = + loadAvatarInternal(user, url, false, null) + +fun ImageView.loadPhoneAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable { + val drawable = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_phone_small) + return loadUserAvatar(drawable) +} + +fun ImageView.loadThumbnail(url: String, user: User): io.reactivex.disposables.Disposable { + val requestBuilder = ImageRequest.Builder(context) + .data(url) + .crossfade(true) + .target(this) + .transformations(CircleCropTransformation()) + + val layers = arrayOfNulls(2) + layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background) + layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground) + requestBuilder.placeholder(LayerDrawable(layers)) + + if (url.startsWith(user.baseUrl!!) && + (url.contains("index.php/core/preview") || url.contains("/avatar/")) + ) { + requestBuilder.addHeader( + "Authorization", + ApiUtils.getCredentials(user.username, user.token)!! + ) + } + + return DisposableWrapper(context.imageLoader.enqueue(requestBuilder.build())) +} + +fun ImageView.loadImage(url: String, user: User, placeholder: Drawable? = null): io.reactivex.disposables.Disposable { + var finalPlaceholder = placeholder + if (finalPlaceholder == null) { + finalPlaceholder = ContextCompat.getDrawable(context!!, R.drawable.ic_mimetype_file) + } + + val requestBuilder = ImageRequest.Builder(context) + .data(url) + .crossfade(true) + .target(this) + .placeholder(finalPlaceholder) + .error(finalPlaceholder) + .transformations(RoundedCornersTransformation(ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL)) + + if (url.startsWith(user.baseUrl!!) && + (url.contains("index.php/core/preview") || url.contains("/avatar/")) + ) { + requestBuilder.addHeader( + "Authorization", + ApiUtils.getCredentials(user.username, user.token)!! + ) + } + + return DisposableWrapper(context.imageLoader.enqueue(requestBuilder.build())) +} + +fun ImageView.loadAvatarOrImagePreview( + url: String, + user: User, + placeholder: Drawable? = null +): io.reactivex.disposables.Disposable = + if (url.contains("/avatar/")) { + loadAvatarInternal(user, url, false, null) + } else { + loadImage(url, user, placeholder) + } + +fun ImageView.loadUserAvatar(any: Any?): io.reactivex.disposables.Disposable = + DisposableWrapper( + load(any) { + transformations(CircleCropTransformation()) + } + ) + +fun ImageView.loadSystemAvatar(): io.reactivex.disposables.Disposable { + val layers = arrayOfNulls(2) + layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background) + layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground) + val layerDrawable = LayerDrawable(layers) + val data: Any = layerDrawable + + return DisposableWrapper( + load(data) { + transformations(CircleCropTransformation()) + } + ) +} + +fun ImageView.loadNoteToSelfAvatar() { + val layers = arrayOfNulls(2) + layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background) + layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_note_to_self) + val layerDrawable = LayerDrawable(layers) + setImageDrawable(CircularDrawable(layerDrawable)) +} + +fun ImageView.loadFirstLetterAvatar(name: String): io.reactivex.disposables.Disposable { + val layers = arrayOfNulls(2) + layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background) + layers[1] = createTextDrawable(context, name.trimStart().uppercase(Locale.ROOT)) + + val layerDrawable = LayerDrawable(layers) + val data: Any = layerDrawable + + return DisposableWrapper( + load(data) { + transformations(CircleCropTransformation()) + } + ) +} + +fun ImageView.loadChangelogBotAvatar(): io.reactivex.disposables.Disposable = loadSystemAvatar() + +fun ImageView.loadBotsAvatar(): io.reactivex.disposables.Disposable { + val layers = arrayOfNulls(2) + layers[0] = context.getColor(R.color.black).toDrawable() + layers[1] = TextDrawable(context, ">") + val layerDrawable = LayerDrawable(layers) + val data: Any = layerDrawable + + return DisposableWrapper( + load(data) { + transformations(CircleCropTransformation()) + } + ) +} + +fun ImageView.loadDefaultGroupCallAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable { + val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_group_small) as Any + return loadUserAvatar(data) +} + +fun ImageView.loadTeamAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable { + val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_team_small) as Any + return loadUserAvatar(data) +} + +fun ImageView.loadDefaultAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable { + val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.account_circle_96dp) as Any + return loadUserAvatar(data) +} + +fun ImageView.loadDefaultPublicCallAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable { + val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_link) as Any + return loadUserAvatar(data) +} + +fun ImageView.loadMailAvatar(viewThemeUtils: ViewThemeUtils): io.reactivex.disposables.Disposable { + val data: Any = viewThemeUtils.talk.themePlaceholderAvatar(this, R.drawable.ic_avatar_mail) as Any + return loadUserAvatar(data) +} + +fun ImageView.loadGuestAvatar(user: User, name: String, big: Boolean): io.reactivex.disposables.Disposable = + loadGuestAvatar(user.baseUrl!!, name, big) + +fun ImageView.loadGuestAvatar(baseUrl: String, name: String, big: Boolean): io.reactivex.disposables.Disposable { + val imageRequestUri = ApiUtils.getUrlForGuestAvatar( + baseUrl, + name, + big + ) + return DisposableWrapper( + load(imageRequestUri) { + transformations(CircleCropTransformation()) + listener(onError = { _, result -> + Log.w(TAG, "Can't load guest avatar with URL: $imageRequestUri", result.throwable) + }) + } + ) +} + +@Suppress("MagicNumber") +private fun createTextDrawable(context: Context, letter: String): Drawable { + val size = 100 + val bitmap = createBitmap(size, size) + val canvas = Canvas(bitmap) + + val paint = Paint().apply { + color = ResourcesCompat.getColor(context.resources, R.color.grey_600, null) + style = Paint.Style.FILL + } + canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), paint) + + val textPaint = Paint().apply { + color = Color.WHITE + textSize = size / 2f + isAntiAlias = true + textAlign = Paint.Align.CENTER + } + + val xPos = size / 2f + val yPos = (canvas.height / 2 - (textPaint.descent() + textPaint.ascent()) / 2) + canvas.drawText(letter.take(1), xPos, yPos, textPaint) + + return bitmap.toDrawable(context.resources) +} + +private class DisposableWrapper(private val disposable: coil.request.Disposable) : + io.reactivex.disposables + .Disposable { + + override fun dispose() { + disposable.dispose() + } + + override fun isDisposed(): Boolean = disposable.isDisposed +} + +private class CircularDrawable(private val sourceDrawable: Drawable) : Drawable() { + + private val bitmap: Bitmap = drawableToBitmap(sourceDrawable) + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + } + + private val rect = RectF() + private var radius = 0f + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + rect.set(bounds) + + radius = min(rect.width() / 2.0f, rect.height() / 2.0f) + + val matrix = Matrix() + val scale: Float + var dx = 0f + var dy = 0f + + if (bitmap.width * rect.height() > rect.width() * bitmap.height) { + // Taller than wide, scale to height and center horizontally + scale = rect.height() / bitmap.height.toFloat() + dx = (rect.width() - bitmap.width * scale) / 2.0f + } else { + // Wider than tall, scale to width and center vertically + scale = rect.width() / bitmap.width.toFloat() + dy = (rect.height() - bitmap.height * scale) / 2.0f + } + + matrix.setScale(scale, scale) + matrix.postTranslate(dx.toInt().toFloat() + rect.left, dy.toInt().toFloat() + rect.top) + paint.shader.setLocalMatrix(matrix) + } + + override fun draw(canvas: Canvas) { + canvas.drawCircle(rect.centerX(), rect.centerY(), radius, paint) + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.colorFilter = colorFilter + } + + @Deprecated( + "This method is no longer used in graphics optimizations", + ReplaceWith("PixelFormat.TRANSLUCENT", "android.graphics.PixelFormat") + ) + override fun getOpacity(): Int = PixelFormat.TRANSLUCENT + + override fun getIntrinsicWidth(): Int = sourceDrawable.intrinsicWidth + + override fun getIntrinsicHeight(): Int = sourceDrawable.intrinsicHeight + + companion object { + + private fun drawableToBitmap(drawable: Drawable): Bitmap { + if (drawable is BitmapDrawable) { + if (drawable.bitmap != null) { + return drawable.bitmap + } + } + + val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else 1 + val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else 1 + return drawable.toBitmap(width, height, Bitmap.Config.ARGB_8888) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/extensions/LongFormatExtension.kt b/app/src/main/java/com/nextcloud/talk/extensions/LongFormatExtension.kt new file mode 100644 index 0000000..4663860 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/extensions/LongFormatExtension.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.extensions + +fun Long.toIntOrZero(): Int = + if (this >= Int.MIN_VALUE && this <= Int.MAX_VALUE) { + toInt() + } else { + 0 + } diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ParcelableExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ParcelableExtensions.kt new file mode 100644 index 0000000..25f68ef --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/extensions/ParcelableExtensions.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.extensions + +import android.content.Intent +import android.os.Build +import android.os.Parcelable + +@Suppress("DEPRECATION") +inline fun Intent.getParcelableExtraProvider(identifierParameter: String): T? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + this.getParcelableExtra(identifierParameter, T::class.java) + } else { + this.getParcelableExtra(identifierParameter) + } + +@Suppress("DEPRECATION") +inline fun Intent.getParcelableArrayListExtraProvider( + identifierParameter: String +): java.util.ArrayList? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + this.getParcelableArrayListExtra(identifierParameter, T::class.java) + } else { + this.getParcelableArrayListExtra(identifierParameter) + } diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/models/BrowserFile.kt b/app/src/main/java/com/nextcloud/talk/filebrowser/models/BrowserFile.kt new file mode 100644 index 0000000..594c680 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/models/BrowserFile.kt @@ -0,0 +1,106 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.models + +import android.net.Uri +import android.os.Parcelable +import android.text.TextUtils +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.property.DisplayName +import at.bitfire.dav4jvm.property.GetContentType +import at.bitfire.dav4jvm.property.GetLastModified +import at.bitfire.dav4jvm.property.ResourceType +import at.bitfire.dav4jvm.property.ResourceType.Companion.COLLECTION +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.filebrowser.models.properties.NCEncrypted +import com.nextcloud.talk.filebrowser.models.properties.NCPermission +import com.nextcloud.talk.filebrowser.models.properties.NCPreview +import com.nextcloud.talk.filebrowser.models.properties.OCFavorite +import com.nextcloud.talk.filebrowser.models.properties.OCId +import com.nextcloud.talk.filebrowser.models.properties.OCSize +import com.nextcloud.talk.utils.Mimetype.FOLDER +import kotlinx.parcelize.Parcelize +import java.io.File + +@Parcelize +@JsonObject +data class BrowserFile( + var path: String? = null, + var displayName: String? = null, + var mimeType: String? = null, + var modifiedTimestamp: Long = 0, + var size: Long = 0, + var isFile: Boolean = false, + + // Used for remote files + var remoteId: String? = null, + var hasPreview: Boolean = false, + var isFavorite: Boolean = false, + var isEncrypted: Boolean = false, + var permissions: String? = null, + var isAllowedToReShare: Boolean = false +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, 0, 0, false, null, false, false, false, null, false) + + companion object { + fun getModelFromResponse(response: Response, remotePath: String): BrowserFile { + val browserFile = BrowserFile() + browserFile.path = Uri.decode(remotePath) + browserFile.displayName = Uri.decode(File(remotePath).name) + val properties = response.properties + for (property in properties) { + mapPropertyToBrowserFile(property, browserFile) + } + if (browserFile.permissions != null && browserFile.permissions!!.contains("R")) { + browserFile.isAllowedToReShare = true + } + if (TextUtils.isEmpty(browserFile.mimeType) && !browserFile.isFile) { + browserFile.mimeType = FOLDER + } + + return browserFile + } + + @Suppress("Detekt.ComplexMethod") + private fun mapPropertyToBrowserFile(property: Property, browserFile: BrowserFile) { + when (property) { + is OCId -> { + browserFile.remoteId = property.ocId + } + is ResourceType -> { + browserFile.isFile = !property.types.contains(COLLECTION) + } + is GetLastModified -> { + browserFile.modifiedTimestamp = property.lastModified + } + is GetContentType -> { + browserFile.mimeType = property.type + } + is OCSize -> { + browserFile.size = property.ocSize + } + is NCPreview -> { + browserFile.hasPreview = property.isNcPreview + } + is OCFavorite -> { + browserFile.isFavorite = property.isOcFavorite + } + is DisplayName -> { + browserFile.displayName = property.displayName + } + is NCEncrypted -> { + browserFile.isEncrypted = property.isNcEncrypted + } + is NCPermission -> { + browserFile.permissions = property.ncPermission + } + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/models/DavResponse.java b/app/src/main/java/com/nextcloud/talk/filebrowser/models/DavResponse.java new file mode 100644 index 0000000..16f50c2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/models/DavResponse.java @@ -0,0 +1,69 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.models; + +import at.bitfire.dav4jvm.Response; + +public class DavResponse { + public Response response; + public Object data; + + public Response getResponse() { + return this.response; + } + + public Object getData() { + return this.data; + } + + public void setResponse(Response response) { + this.response = response; + } + + public void setData(Object data) { + this.data = data; + } + + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof DavResponse)) { + return false; + } + final DavResponse other = (DavResponse) o; + if (!other.canEqual((Object) this)) { + return false; + } + final Object this$response = this.getResponse(); + final Object other$response = other.getResponse(); + if (this$response == null ? other$response != null : !this$response.equals(other$response)) { + return false; + } + final Object this$data = this.getData(); + final Object other$data = other.getData(); + + return this$data == null ? other$data == null : this$data.equals(other$data); + } + + protected boolean canEqual(final Object other) { + return other instanceof DavResponse; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $response = this.getResponse(); + result = result * PRIME + ($response == null ? 43 : $response.hashCode()); + final Object $data = this.getData(); + return result * PRIME + ($data == null ? 43 : $data.hashCode()); + } + + public String toString() { + return "DavResponse(response=" + this.getResponse() + ", data=" + this.getData() + ")"; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCEncrypted.kt b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCEncrypted.kt new file mode 100644 index 0000000..86fb7e8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCEncrypted.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.models.properties + +import android.text.TextUtils +import android.util.Log +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.PropertyFactory +import at.bitfire.dav4jvm.XmlUtils.readText +import com.nextcloud.talk.filebrowser.webdav.DavUtils +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException + +class NCEncrypted private constructor(var isNcEncrypted: Boolean) : Property { + + class Factory : PropertyFactory { + override fun create(parser: XmlPullParser): Property { + try { + val text = readText(parser) + if (!TextUtils.isEmpty(text)) { + return NCEncrypted("1" == text) + } + } catch (e: IOException) { + Log.e("NCEncrypted", "failed to create property", e) + } catch (e: XmlPullParserException) { + Log.e("NCEncrypted", "failed to create property", e) + } + return NCEncrypted(false) + } + + override fun getName(): Property.Name = NAME + } + + companion object { + @JvmField + val NAME: Property.Name = Property.Name(DavUtils.NC_NAMESPACE, DavUtils.EXTENDED_PROPERTY_IS_ENCRYPTED) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCPermission.kt b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCPermission.kt new file mode 100644 index 0000000..7c54d55 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCPermission.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.models.properties + +import android.text.TextUtils +import android.util.Log +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.PropertyFactory +import at.bitfire.dav4jvm.XmlUtils.readText +import com.nextcloud.talk.filebrowser.webdav.DavUtils +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException + +class NCPermission private constructor(var ncPermission: String?) : Property { + + class Factory : PropertyFactory { + override fun create(parser: XmlPullParser): Property { + try { + val text = readText(parser) + if (!TextUtils.isEmpty(text)) { + return NCPermission(text) + } + } catch (e: IOException) { + Log.e("NCPermission", "failed to create property", e) + } catch (e: XmlPullParserException) { + Log.e("NCPermission", "failed to create property", e) + } + return NCPermission("") + } + + override fun getName(): Property.Name = NAME + } + + companion object { + @JvmField + val NAME: Property.Name = Property.Name(DavUtils.OC_NAMESPACE, DavUtils.EXTENDED_PROPERTY_NAME_PERMISSIONS) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCPreview.kt b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCPreview.kt new file mode 100644 index 0000000..0089ef0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/NCPreview.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.models.properties + +import android.text.TextUtils +import android.util.Log +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.PropertyFactory +import at.bitfire.dav4jvm.XmlUtils.readText +import com.nextcloud.talk.filebrowser.webdav.DavUtils +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException + +class NCPreview private constructor(var isNcPreview: Boolean) : Property { + + class Factory : PropertyFactory { + override fun create(parser: XmlPullParser): Property { + try { + val text = readText(parser) + if (!TextUtils.isEmpty(text)) { + return NCPreview(java.lang.Boolean.parseBoolean(text)) + } + } catch (e: IOException) { + Log.e("NCPreview", "failed to create property", e) + } catch (e: XmlPullParserException) { + Log.e("NCPreview", "failed to create property", e) + } + return OCFavorite(false) + } + + override fun getName(): Property.Name = NAME + } + + companion object { + @JvmField + val NAME: Property.Name = Property.Name(DavUtils.NC_NAMESPACE, DavUtils.EXTENDED_PROPERTY_HAS_PREVIEW) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCFavorite.kt b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCFavorite.kt new file mode 100644 index 0000000..44a3f41 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCFavorite.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.models.properties + +import android.text.TextUtils +import android.util.Log +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.PropertyFactory +import at.bitfire.dav4jvm.XmlUtils.readText +import com.nextcloud.talk.filebrowser.webdav.DavUtils +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException + +class OCFavorite internal constructor(var isOcFavorite: Boolean) : Property { + + class Factory : PropertyFactory { + override fun create(parser: XmlPullParser): Property { + try { + val text = readText(parser) + if (!TextUtils.isEmpty(text)) { + return OCFavorite("1" == text) + } + } catch (e: IOException) { + Log.e("OCFavorite", "failed to create property", e) + } catch (e: XmlPullParserException) { + Log.e("OCFavorite", "failed to create property", e) + } + return OCFavorite(false) + } + + override fun getName(): Property.Name = NAME + } + + companion object { + @JvmField + val NAME: Property.Name = Property.Name(DavUtils.OC_NAMESPACE, DavUtils.EXTENDED_PROPERTY_FAVORITE) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCId.kt b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCId.kt new file mode 100644 index 0000000..80e80f7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCId.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.models.properties + +import android.text.TextUtils +import android.util.Log +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.PropertyFactory +import at.bitfire.dav4jvm.XmlUtils.readText +import com.nextcloud.talk.filebrowser.webdav.DavUtils +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException + +class OCId private constructor(var ocId: String?) : Property { + + class Factory : PropertyFactory { + override fun create(parser: XmlPullParser): Property { + try { + val text = readText(parser) + if (!TextUtils.isEmpty(text)) { + return OCId(text) + } + } catch (e: IOException) { + Log.e("OCId", "failed to create property", e) + } catch (e: XmlPullParserException) { + Log.e("OCId", "failed to create property", e) + } + return OCId("") + } + + override fun getName(): Property.Name = NAME + } + + companion object { + @JvmField + val NAME: Property.Name = Property.Name(DavUtils.OC_NAMESPACE, DavUtils.EXTENDED_PROPERTY_NAME_REMOTE_ID) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCSize.kt b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCSize.kt new file mode 100644 index 0000000..d83a3d1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/models/properties/OCSize.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.models.properties + +import android.text.TextUtils +import android.util.Log +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.PropertyFactory +import at.bitfire.dav4jvm.XmlUtils.readText +import com.nextcloud.talk.filebrowser.webdav.DavUtils +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException + +class OCSize private constructor(var ocSize: Long) : Property { + + class Factory : PropertyFactory { + override fun create(parser: XmlPullParser): Property { + try { + val text = readText(parser) + if (!TextUtils.isEmpty(text)) { + return OCSize(text!!.toLong()) + } + } catch (e: IOException) { + Log.e("OCSize", "failed to create property", e) + } catch (e: XmlPullParserException) { + Log.e("OCSize", "failed to create property", e) + } + return OCSize(-1) + } + + override fun getName(): Property.Name = NAME + } + + companion object { + @JvmField + val NAME: Property.Name = Property.Name(DavUtils.OC_NAMESPACE, DavUtils.EXTENDED_PROPERTY_NAME_SIZE) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/webdav/DavUtils.java b/app/src/main/java/com/nextcloud/talk/filebrowser/webdav/DavUtils.java new file mode 100644 index 0000000..347ea5a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/webdav/DavUtils.java @@ -0,0 +1,94 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.webdav; + +import com.nextcloud.talk.filebrowser.models.properties.NCEncrypted; +import com.nextcloud.talk.filebrowser.models.properties.NCPermission; +import com.nextcloud.talk.filebrowser.models.properties.NCPreview; +import com.nextcloud.talk.filebrowser.models.properties.OCFavorite; +import com.nextcloud.talk.filebrowser.models.properties.OCId; +import com.nextcloud.talk.filebrowser.models.properties.OCSize; + +import java.util.ArrayList; +import java.util.List; + +import at.bitfire.dav4jvm.Property; +import at.bitfire.dav4jvm.PropertyRegistry; +import at.bitfire.dav4jvm.property.CreationDate; +import at.bitfire.dav4jvm.property.DisplayName; +import at.bitfire.dav4jvm.property.GetContentLength; +import at.bitfire.dav4jvm.property.GetContentType; +import at.bitfire.dav4jvm.property.GetETag; +import at.bitfire.dav4jvm.property.GetLastModified; +import at.bitfire.dav4jvm.property.ResourceType; + +public class DavUtils { + + public static final String OC_NAMESPACE = "http://owncloud.org/ns"; + public static final String NC_NAMESPACE = "http://nextcloud.org/ns"; + public static final String DAV_PATH = "/remote.php/dav/files/"; + + public static final String EXTENDED_PROPERTY_NAME_PERMISSIONS = "permissions"; + public static final String EXTENDED_PROPERTY_NAME_REMOTE_ID = "id"; + public static final String EXTENDED_PROPERTY_NAME_SIZE = "size"; + public static final String EXTENDED_PROPERTY_FAVORITE = "favorite"; + public static final String EXTENDED_PROPERTY_IS_ENCRYPTED = "is-encrypted"; + public static final String EXTENDED_PROPERTY_MOUNT_TYPE = "mount-type"; + public static final String EXTENDED_PROPERTY_OWNER_ID = "owner-id"; + public static final String EXTENDED_PROPERTY_OWNER_DISPLAY_NAME = "owner-display-name"; + public static final String EXTENDED_PROPERTY_UNREAD_COMMENTS = "comments-unread"; + public static final String EXTENDED_PROPERTY_HAS_PREVIEW = "has-preview"; + public static final String EXTENDED_PROPERTY_NOTE = "note"; + + // public static final String TRASHBIN_FILENAME = "trashbin-filename"; + // public static final String TRASHBIN_ORIGINAL_LOCATION = "trashbin-original-location"; + // public static final String TRASHBIN_DELETION_TIME = "trashbin-deletion-time"; + + // public static final String PROPERTY_QUOTA_USED_BYTES = "quota-used-bytes"; + // public static final String PROPERTY_QUOTA_AVAILABLE_BYTES = "quota-available-bytes"; + + static Property.Name[] getAllPropSet() { + List props = new ArrayList<>(20); + + props.add(DisplayName.NAME); + props.add(GetContentType.NAME); + props.add(GetContentLength.NAME); + props.add(GetContentType.NAME); + props.add(GetContentLength.NAME); + props.add(GetLastModified.NAME); + props.add(CreationDate.NAME); + props.add(GetETag.NAME); + props.add(ResourceType.NAME); + + props.add(NCPermission.NAME); + props.add(OCId.NAME); + props.add(OCSize.NAME); + props.add(OCFavorite.NAME); + props.add(new Property.Name(OC_NAMESPACE, EXTENDED_PROPERTY_OWNER_ID)); + props.add(new Property.Name(OC_NAMESPACE, EXTENDED_PROPERTY_OWNER_DISPLAY_NAME)); + props.add(new Property.Name(OC_NAMESPACE, EXTENDED_PROPERTY_UNREAD_COMMENTS)); + + props.add(NCEncrypted.NAME); + props.add(new Property.Name(NC_NAMESPACE, EXTENDED_PROPERTY_MOUNT_TYPE)); + props.add(NCPreview.NAME); + props.add(new Property.Name(NC_NAMESPACE, EXTENDED_PROPERTY_NOTE)); + + return props.toArray(new Property.Name[0]); + } + + public static void registerCustomFactories() { + PropertyRegistry propertyRegistry = PropertyRegistry.INSTANCE; + + propertyRegistry.register(new OCId.Factory()); + propertyRegistry.register(new NCPreview.Factory()); + propertyRegistry.register(new NCEncrypted.Factory()); + propertyRegistry.register(new OCFavorite.Factory()); + propertyRegistry.register(new OCSize.Factory()); + propertyRegistry.register(new NCPermission.Factory()); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/webdav/ReadFilesystemOperation.java b/app/src/main/java/com/nextcloud/talk/filebrowser/webdav/ReadFilesystemOperation.java new file mode 100644 index 0000000..618820f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/webdav/ReadFilesystemOperation.java @@ -0,0 +1,93 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.webdav; + +import android.util.Log; + +import com.nextcloud.talk.filebrowser.models.BrowserFile; +import com.nextcloud.talk.filebrowser.models.DavResponse; +import com.nextcloud.talk.dagger.modules.RestModule; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.utils.ApiUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import at.bitfire.dav4jvm.DavResource; +import at.bitfire.dav4jvm.Response; +import at.bitfire.dav4jvm.exception.DavException; +import kotlin.Unit; +import kotlin.jvm.functions.Function2; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; + +public class ReadFilesystemOperation { + private static final String TAG = "ReadFilesystemOperation"; + private final OkHttpClient okHttpClient; + private final String url; + private final int depth; + private final String basePath; + + public ReadFilesystemOperation(OkHttpClient okHttpClient, User currentUser, String path, int depth) { + OkHttpClient.Builder okHttpClientBuilder = okHttpClient.newBuilder(); + okHttpClientBuilder.followRedirects(false); + okHttpClientBuilder.followSslRedirects(false); + okHttpClientBuilder.authenticator( + new RestModule.HttpAuthenticator( + ApiUtils.getCredentials( + currentUser.getUsername(), + currentUser.getToken() + ), + "Authorization") + ); + this.okHttpClient = okHttpClientBuilder.build(); + this.basePath = currentUser.getBaseUrl() + DavUtils.DAV_PATH + currentUser.getUserId(); + this.url = basePath + path; + this.depth = depth; + } + + public DavResponse readRemotePath() { + DavResponse davResponse = new DavResponse(); + final List memberElements = new ArrayList<>(); + final Response[] rootElement = new Response[1]; + + try { + new DavResource(okHttpClient, HttpUrl.parse(url)).propfind(depth, DavUtils.getAllPropSet(), + new Function2() { + @Override + public Unit invoke(Response response, Response.HrefRelation hrefRelation) { + davResponse.setResponse(response); + switch (hrefRelation) { + case MEMBER: + memberElements.add(response); + break; + case SELF: + rootElement[0] = response; + break; + case OTHER: + default: + } + return Unit.INSTANCE; + } + }); + } catch (IOException | DavException e) { + Log.w(TAG, "Error reading remote path"); + } + + final List remoteFiles = new ArrayList<>(1 + memberElements.size()); + remoteFiles.add(BrowserFile.Companion.getModelFromResponse(rootElement[0], + rootElement[0].getHref().toString().substring(basePath.length()))); + for (Response memberElement : memberElements) { + remoteFiles.add(BrowserFile.Companion.getModelFromResponse(memberElement, + memberElement.getHref().toString().substring(basePath.length()))); + } + + davResponse.setData(remoteFiles); + return davResponse; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/filebrowser/webdav/ReadFolderListingOperation.kt b/app/src/main/java/com/nextcloud/talk/filebrowser/webdav/ReadFolderListingOperation.kt new file mode 100644 index 0000000..4c75753 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/filebrowser/webdav/ReadFolderListingOperation.kt @@ -0,0 +1,166 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.filebrowser.webdav + +import android.net.Uri +import android.text.TextUtils +import android.util.Log +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.Response +import at.bitfire.dav4jvm.Response.HrefRelation +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.property.DisplayName +import at.bitfire.dav4jvm.property.GetContentType +import at.bitfire.dav4jvm.property.GetLastModified +import at.bitfire.dav4jvm.property.ResourceType +import com.nextcloud.talk.filebrowser.models.DavResponse +import com.nextcloud.talk.filebrowser.models.properties.NCEncrypted +import com.nextcloud.talk.filebrowser.models.properties.NCPermission +import com.nextcloud.talk.filebrowser.models.properties.NCPreview +import com.nextcloud.talk.filebrowser.models.properties.OCFavorite +import com.nextcloud.talk.filebrowser.models.properties.OCId +import com.nextcloud.talk.filebrowser.models.properties.OCSize +import com.nextcloud.talk.dagger.modules.RestModule.HttpAuthenticator +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.Mimetype.FOLDER +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import java.io.File +import java.io.IOException + +class ReadFolderListingOperation(okHttpClient: OkHttpClient, currentUser: User, path: String, depth: Int) { + private val okHttpClient: OkHttpClient + private val url: String + private val depth: Int + private val basePath: String + + init { + val okHttpClientBuilder: OkHttpClient.Builder = okHttpClient.newBuilder() + okHttpClientBuilder.followRedirects(false) + okHttpClientBuilder.followSslRedirects(false) + okHttpClientBuilder.authenticator( + HttpAuthenticator( + ApiUtils.getCredentials( + currentUser.username, + currentUser.token + )!!, + "Authorization" + ) + ) + this.okHttpClient = okHttpClientBuilder.build() + basePath = currentUser.baseUrl + DavUtils.DAV_PATH + currentUser.userId + url = basePath + path + this.depth = depth + } + + fun readRemotePath(): DavResponse { + val davResponse = DavResponse() + val memberElements: MutableList = ArrayList() + val rootElement = arrayOfNulls(1) + val remoteFiles: MutableList = ArrayList() + try { + DavResource( + okHttpClient, + url.toHttpUrlOrNull()!! + ).propfind( + depth = depth, + reqProp = DavUtils.getAllPropSet() + ) { response: Response, hrefRelation: HrefRelation? -> + davResponse.setResponse(response) + when (hrefRelation) { + HrefRelation.MEMBER -> memberElements.add(response) + HrefRelation.SELF -> rootElement[0] = response + HrefRelation.OTHER -> {} + else -> {} + } + Unit + } + } catch (e: IOException) { + Log.w(TAG, "Error reading remote path") + } catch (e: DavException) { + Log.w(TAG, "Error reading remote path") + } + for (memberElement in memberElements) { + remoteFiles.add( + getModelFromResponse( + memberElement, + memberElement + .href + .toString() + .substring(basePath.length) + ) + ) + } + davResponse.setData(remoteFiles) + return davResponse + } + + private fun getModelFromResponse(response: Response, remotePath: String): RemoteFileBrowserItem { + val remoteFileBrowserItem = RemoteFileBrowserItem() + remoteFileBrowserItem.path = Uri.decode(remotePath) + remoteFileBrowserItem.displayName = Uri.decode(File(remotePath).name) + val properties = response.properties + for (property in properties) { + mapPropertyToBrowserFile(property, remoteFileBrowserItem) + } + if (remoteFileBrowserItem.permissions != null && + remoteFileBrowserItem.permissions!!.contains(READ_PERMISSION) + ) { + remoteFileBrowserItem.isAllowedToReShare = true + } + if (TextUtils.isEmpty(remoteFileBrowserItem.mimeType) && !remoteFileBrowserItem.isFile) { + remoteFileBrowserItem.mimeType = FOLDER + } + + return remoteFileBrowserItem + } + + @Suppress("Detekt.ComplexMethod") + private fun mapPropertyToBrowserFile(property: Property, remoteFileBrowserItem: RemoteFileBrowserItem) { + when (property) { + is OCId -> { + remoteFileBrowserItem.remoteId = property.ocId + } + is ResourceType -> { + remoteFileBrowserItem.isFile = !property.types.contains(ResourceType.COLLECTION) + } + is GetLastModified -> { + remoteFileBrowserItem.modifiedTimestamp = property.lastModified + } + is GetContentType -> { + remoteFileBrowserItem.mimeType = property.type + } + is OCSize -> { + remoteFileBrowserItem.size = property.ocSize + } + is NCPreview -> { + remoteFileBrowserItem.hasPreview = property.isNcPreview + } + is OCFavorite -> { + remoteFileBrowserItem.isFavorite = property.isOcFavorite + } + is DisplayName -> { + remoteFileBrowserItem.displayName = property.displayName + } + is NCEncrypted -> { + remoteFileBrowserItem.isEncrypted = property.isNcEncrypted + } + is NCPermission -> { + remoteFileBrowserItem.permissions = property.ncPermission + } + } + } + + companion object { + private const val TAG = "ReadFilesystemOperation" + private const val READ_PERMISSION = "R" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenImageActivity.kt b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenImageActivity.kt new file mode 100644 index 0000000..0614d93 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenImageActivity.kt @@ -0,0 +1,198 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Dariusz Olszewski + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.fullscreenfile + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.fragment.app.DialogFragment +import autodagger.AutoInjector +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.ActivityFullScreenImageBinding +import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment +import com.nextcloud.talk.utils.BitmapShrinker +import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC +import pl.droidsonroids.gif.GifDrawable +import java.io.File + +@AutoInjector(NextcloudTalkApplication::class) +class FullScreenImageActivity : AppCompatActivity() { + lateinit var binding: ActivityFullScreenImageBinding + private lateinit var windowInsetsController: WindowInsetsControllerCompat + private lateinit var path: String + private var showFullscreen = false + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_preview, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + android.R.id.home -> { + onBackPressedDispatcher.onBackPressed() + true + } + + R.id.share -> { + val shareUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID, + File(path) + ) + + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, shareUri) + type = IMAGE_PREFIX_GENERIC + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to))) + + true + } + + R.id.save -> { + val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance( + intent.getStringExtra("FILE_NAME").toString() + ) + saveFragment.show( + supportFragmentManager, + SaveToStorageDialogFragment.TAG + ) + true + } + + else -> { + super.onOptionsItemSelected(item) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityFullScreenImageBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.imageviewToolbar) + WindowCompat.setDecorFitsSystemWindows(window, false) + initWindowInsetsController() + applyWindowInsets() + binding.photoView.setOnPhotoTapListener { _, _, _ -> + toggleFullscreen() + } + binding.photoView.setOnOutsidePhotoTapListener { + toggleFullscreen() + } + binding.gifView.setOnClickListener { + toggleFullscreen() + } + + // Enable enlarging the image more than default 3x maximumScale. + // Medium scale adapted to make double-tap behaviour more consistent. + binding.photoView.maximumScale = MAX_SCALE + binding.photoView.mediumScale = MEDIUM_SCALE + + val fileName = intent.getStringExtra("FILE_NAME") + val isGif = intent.getBooleanExtra("IS_GIF", false) + + supportActionBar?.title = fileName + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + path = applicationContext.cacheDir.absolutePath + "/" + fileName + if (isGif) { + binding.photoView.visibility = View.INVISIBLE + binding.gifView.visibility = View.VISIBLE + val gifFromUri = GifDrawable(path) + binding.gifView.setImageDrawable(gifFromUri) + } else { + binding.gifView.visibility = View.INVISIBLE + binding.photoView.visibility = View.VISIBLE + displayImage(path) + } + } + + private fun displayImage(path: String) { + val displayMetrics = applicationContext.resources.displayMetrics + val doubleScreenWidth = displayMetrics.widthPixels * 2 + val doubleScreenHeight = displayMetrics.heightPixels * 2 + + val bitmap = BitmapShrinker.shrinkBitmap(path, doubleScreenWidth, doubleScreenHeight) + + val bitmapSize: Int = bitmap.byteCount + + // info that 100MB is the limit comes from https://stackoverflow.com/a/53334563 + if (bitmapSize > HUNDRED_MB) { + Log.e(TAG, "bitmap will be too large to display. It won't be displayed to avoid RuntimeException") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } else { + binding.photoView.setImageBitmap(bitmap) + } + } + + private fun toggleFullscreen() { + showFullscreen = !showFullscreen + if (showFullscreen) { + enterImmersiveMode() + } else { + exitImmersiveMode() + } + } + + private fun initWindowInsetsController() { + windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + + private fun enterImmersiveMode() { + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) + supportActionBar?.hide() + } + + private fun exitImmersiveMode() { + windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) + supportActionBar?.show() + } + + private fun applyWindowInsets() { + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets -> + val insets = + windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()) + binding.imageviewToolbar.updateLayoutParams { + topMargin = insets.top + } + binding.imageviewToolbar.updatePadding(left = insets.left, right = insets.right) + WindowInsetsCompat.CONSUMED + } + } + + companion object { + private const val TAG = "FullScreenImageActivity" + private const val HUNDRED_MB = 100 * 1024 * 1024 + private const val MAX_SCALE = 6.0f + private const val MEDIUM_SCALE = 2.45f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt new file mode 100644 index 0000000..0bc48e0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt @@ -0,0 +1,219 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2023 Parneet Singh + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.fullscreenfile + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.ViewGroup.MarginLayoutParams +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.annotation.OptIn +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.marginBottom +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import androidx.fragment.app.DialogFragment +import androidx.media3.common.AudioAttributes +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.DefaultTimeBar +import androidx.media3.ui.PlayerView +import autodagger.AutoInjector +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.ActivityFullScreenMediaBinding +import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment +import com.nextcloud.talk.utils.Mimetype.VIDEO_PREFIX_GENERIC +import java.io.File + +@AutoInjector(NextcloudTalkApplication::class) +class FullScreenMediaActivity : AppCompatActivity() { + lateinit var binding: ActivityFullScreenMediaBinding + + private lateinit var path: String + private var player: ExoPlayer? = null + + private var playWhenReadyState: Boolean = true + private var playBackPosition: Long = 0L + private lateinit var windowInsetsController: WindowInsetsControllerCompat + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_preview, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + android.R.id.home -> { + onBackPressedDispatcher.onBackPressed() + true + } + + R.id.share -> { + val shareUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID, + File(path) + ) + + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, shareUri) + type = VIDEO_PREFIX_GENERIC + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to))) + + true + } + + R.id.save -> { + val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance( + intent.getStringExtra("FILE_NAME").toString() + ) + saveFragment.show( + supportFragmentManager, + SaveToStorageDialogFragment.TAG + ) + true + } + + else -> { + super.onOptionsItemSelected(item) + } + } + + @OptIn(UnstableApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + val fileName = intent.getStringExtra("FILE_NAME") + val isAudioOnly = intent.getBooleanExtra("AUDIO_ONLY", false) + + path = applicationContext.cacheDir.absolutePath + "/" + fileName + + binding = ActivityFullScreenMediaBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.mediaviewToolbar) + supportActionBar?.title = fileName + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + binding.playerView.showController() + if (isAudioOnly) { + binding.playerView.controllerShowTimeoutMs = 0 + } + + initWindowInsetsController() + applyWindowInsets() + + binding.playerView.setControllerVisibilityListener( + PlayerView.ControllerVisibilityListener { v -> + if (v != 0) { + enterImmersiveMode() + } else { + exitImmersiveMode() + } + } + ) + } + + override fun onStart() { + super.onStart() + initializePlayer() + preparePlayer() + } + + override fun onStop() { + super.onStop() + releasePlayer() + } + + private fun initializePlayer() { + player = ExoPlayer.Builder(applicationContext) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setHandleAudioBecomingNoisy(true) + .build() + binding.playerView.player = player + } + + private fun preparePlayer() { + val mediaItem: MediaItem = MediaItem.fromUri(path) + player?.let { exoPlayer -> + exoPlayer.setMediaItem(mediaItem) + exoPlayer.playWhenReady = playWhenReadyState + exoPlayer.seekTo(playBackPosition) + exoPlayer.prepare() + } + } + + private fun releasePlayer() { + player?.let { exoPlayer -> + playBackPosition = exoPlayer.currentPosition + playWhenReadyState = exoPlayer.playWhenReady + exoPlayer.release() + } + player = null + } + + private fun initWindowInsetsController() { + windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + + private fun enterImmersiveMode() { + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) + supportActionBar?.hide() + } + + private fun exitImmersiveMode() { + windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) + supportActionBar?.show() + } + + @OptIn(UnstableApi::class) + private fun applyWindowInsets() { + val playerView = binding.playerView + val exoControls = playerView.findViewById(R.id.exo_bottom_bar) + val exoProgress = playerView.findViewById(R.id.exo_progress) + val progressBottomMargin = exoProgress.marginBottom + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets -> + val insets = windowInsets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type + .displayCutout() + ) + binding.mediaviewToolbar.updateLayoutParams { + topMargin = insets.top + } + exoControls.updateLayoutParams { + bottomMargin = insets.bottom + } + exoProgress.updateLayoutParams { + bottomMargin = insets.bottom + progressBottomMargin + } + exoControls.updatePadding(left = insets.left, right = insets.right) + exoProgress.updatePadding(left = insets.left, right = insets.right) + binding.mediaviewToolbar.updatePadding(left = insets.left, right = insets.right) + WindowInsetsCompat.CONSUMED + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt new file mode 100644 index 0000000..00bb5ad --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenTextViewerActivity.kt @@ -0,0 +1,124 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.fullscreenfile + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.FileProvider +import androidx.core.content.res.ResourcesCompat +import androidx.fragment.app.DialogFragment +import autodagger.AutoInjector +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.ActivityFullScreenTextBinding +import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.Mimetype.TEXT_PREFIX_GENERIC +import io.noties.markwon.Markwon +import java.io.File +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class FullScreenTextViewerActivity : AppCompatActivity() { + lateinit var binding: ActivityFullScreenTextBinding + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var path: String + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_preview, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + android.R.id.home -> { + onBackPressedDispatcher.onBackPressed() + true + } + + R.id.share -> { + val shareUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID, + File(path) + ) + + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, shareUri) + type = TEXT_PREFIX_GENERIC + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to))) + + true + } + + R.id.save -> { + val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance( + intent.getStringExtra("FILE_NAME").toString() + ) + saveFragment.show( + supportFragmentManager, + SaveToStorageDialogFragment.TAG + ) + true + } + + else -> { + super.onOptionsItemSelected(item) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityFullScreenTextBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.textviewToolbar) + + val fileName = intent.getStringExtra("FILE_NAME") + val isMarkdown = intent.getBooleanExtra("IS_MARKDOWN", false) + path = applicationContext.cacheDir.absolutePath + "/" + fileName + val text = readFile(path) + + if (isMarkdown) { + val markwon = Markwon.create(applicationContext) + markwon.setMarkdown(binding.textView, text) + } else { + binding.textView.text = text + } + + supportActionBar?.title = fileName + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + viewThemeUtils.platform.themeStatusBar(this) + viewThemeUtils.material.themeToolbar(binding.textviewToolbar) + viewThemeUtils.material.colorToolbarOverflowIcon(binding.textviewToolbar) + + if (resources != null) { + DisplayUtils.applyColorToNavigationBar( + this.window, + ResourcesCompat.getColor(resources, R.color.bg_default, null) + ) + } + } + + private fun readFile(fileName: String) = File(fileName).inputStream().readBytes().toString(Charsets.UTF_8) +} diff --git a/app/src/main/java/com/nextcloud/talk/interfaces/ClosedInterface.kt b/app/src/main/java/com/nextcloud/talk/interfaces/ClosedInterface.kt new file mode 100644 index 0000000..eda9062 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/interfaces/ClosedInterface.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.interfaces + +interface ClosedInterface { + + val isGooglePlayServicesAvailable: Boolean + fun providerInstallerInstallIfNeededAsync() + fun setUpPushTokenRegistration() +} diff --git a/app/src/main/java/com/nextcloud/talk/interfaces/ConversationMenuInterface.kt b/app/src/main/java/com/nextcloud/talk/interfaces/ConversationMenuInterface.kt new file mode 100644 index 0000000..97efb87 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/interfaces/ConversationMenuInterface.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.interfaces + +import android.os.Bundle + +interface ConversationMenuInterface { + fun showDeleteConversationDialog(bundle: Bundle) +} diff --git a/app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt b/app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt new file mode 100644 index 0000000..40cfc61 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt @@ -0,0 +1,174 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.invitation + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.core.graphics.drawable.toDrawable +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.conversationlist.ConversationsListActivity +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityInvitationsBinding +import com.nextcloud.talk.invitation.adapters.InvitationsAdapter +import com.nextcloud.talk.invitation.data.ActionEnum +import com.nextcloud.talk.invitation.data.Invitation +import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel +import com.nextcloud.talk.utils.bundle.BundleKeys +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class InvitationsActivity : BaseActivity() { + + private lateinit var binding: ActivityInvitationsBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + lateinit var invitationsViewModel: InvitationsViewModel + + lateinit var adapter: InvitationsAdapter + + private lateinit var currentUser: User + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val intent = Intent(this@InvitationsActivity, ConversationsListActivity::class.java) + startActivity(intent) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + invitationsViewModel = ViewModelProvider(this, viewModelFactory)[InvitationsViewModel::class.java] + + currentUser = currentUserProvider.currentUser.blockingGet() + invitationsViewModel.fetchInvitations(currentUser) + + binding = ActivityInvitationsBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + initSystemBars() + + adapter = InvitationsAdapter(currentUser) { invitation, action -> + handleInvitation(invitation, action) + } + + binding.invitationsRecyclerView.adapter = adapter + + initObservers() + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + enum class InvitationAction { + ACCEPT, + REJECT + } + + private fun handleInvitation(invitation: Invitation, action: InvitationAction) { + when (action) { + InvitationAction.ACCEPT -> { + invitationsViewModel.acceptInvitation(currentUser, invitation) + } + + InvitationAction.REJECT -> { + invitationsViewModel.rejectInvitation(currentUser, invitation) + } + } + } + + private fun initObservers() { + invitationsViewModel.fetchInvitationsViewState.observe(this) { state -> + when (state) { + is InvitationsViewModel.FetchInvitationsStartState -> { + binding.invitationsRecyclerView.visibility = View.GONE + binding.progressBarWrapper.visibility = View.VISIBLE + } + + is InvitationsViewModel.FetchInvitationsSuccessState -> { + binding.invitationsRecyclerView.visibility = View.VISIBLE + binding.progressBarWrapper.visibility = View.GONE + adapter.submitList(state.invitations) + } + + is InvitationsViewModel.FetchInvitationsEmptyState -> { + binding.invitationsRecyclerView.visibility = View.GONE + binding.progressBarWrapper.visibility = View.GONE + + binding.emptyList.emptyListView.visibility = View.VISIBLE + binding.emptyList.emptyListViewHeadline.text = getString(R.string.nc_federation_no_invitations) + binding.emptyList.emptyListIcon.setImageResource(R.drawable.baseline_info_24) + binding.emptyList.emptyListIcon.visibility = View.VISIBLE + binding.emptyList.emptyListViewText.visibility = View.VISIBLE + } + + is InvitationsViewModel.FetchInvitationsErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + + invitationsViewModel.invitationActionViewState.observe(this) { state -> + when (state) { + is InvitationsViewModel.InvitationActionStartState -> { + } + + is InvitationsViewModel.InvitationActionSuccessState -> { + if (state.action == ActionEnum.ACCEPT) { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, state.invitation.localToken) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(chatIntent) + } else { + // adapter.currentList.remove(state.invitation) + // adapter.notifyDataSetChanged() // leads to UnsupportedOperationException ?! + + // Just reload activity as lazy workaround to not deal with adapter for now. + // Might be fine until switching to jetpack compose. + finish() + startActivity(intent) + } + } + + is InvitationsViewModel.InvitationActionErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + } + + private fun setupActionBar() { + setSupportActionBar(binding.invitationsToolbar) + binding.invitationsToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(R.color.transparent, null).toDrawable()) + viewThemeUtils.material.themeToolbar(binding.invitationsToolbar) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt b/app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt new file mode 100644 index 0000000..104ea73 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt @@ -0,0 +1,86 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.invitation.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.RvItemInvitationBinding +import com.nextcloud.talk.invitation.InvitationsActivity +import com.nextcloud.talk.invitation.data.Invitation +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class InvitationsAdapter( + val user: User, + private val handleInvitation: (Invitation, InvitationsActivity.InvitationAction) -> Unit +) : ListAdapter(InvitationsCallback) { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + inner class InvitationsViewHolder(private val itemBinding: RvItemInvitationBinding) : + RecyclerView.ViewHolder(itemBinding.root) { + + private var currentInvitation: Invitation? = null + + fun bindItem(invitation: Invitation) { + currentInvitation = invitation + + itemBinding.title.text = invitation.roomName + itemBinding.subject.text = String.format( + itemBinding.root.context.resources.getString(R.string.nc_federation_invited_to_room), + invitation.inviterDisplayName, + invitation.remoteServerUrl + ) + + itemBinding.acceptInvitation.setOnClickListener { + currentInvitation?.let { + handleInvitation(it, InvitationsActivity.InvitationAction.ACCEPT) + } + } + + itemBinding.rejectInvitation.setOnClickListener { + currentInvitation?.let { + handleInvitation(it, InvitationsActivity.InvitationAction.REJECT) + } + } + + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(itemBinding.rejectInvitation) + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(itemBinding.acceptInvitation) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InvitationsViewHolder { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + return InvitationsViewHolder( + RvItemInvitationBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: InvitationsViewHolder, position: Int) { + val invitation = getItem(position) + holder.bindItem(invitation) + } +} + +object InvitationsCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Invitation, newItem: Invitation): Boolean = oldItem == newItem + + override fun areContentsTheSame(oldItem: Invitation, newItem: Invitation): Boolean = oldItem.id == newItem.id +} diff --git a/app/src/main/java/com/nextcloud/talk/invitation/data/Invitation.kt b/app/src/main/java/com/nextcloud/talk/invitation/data/Invitation.kt new file mode 100644 index 0000000..9234a93 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/data/Invitation.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.invitation.data + +data class Invitation( + var id: Int = 0, + var state: Int = 0, + var localCloudId: String? = null, + var localToken: String? = null, + var remoteAttendeeId: Int = 0, + var remoteServerUrl: String? = null, + var remoteToken: String? = null, + var roomName: String? = null, + var userId: String? = null, + var inviterCloudId: String? = null, + var inviterDisplayName: String? = null +) diff --git a/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationActionModel.kt b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationActionModel.kt new file mode 100644 index 0000000..5aab1e4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationActionModel.kt @@ -0,0 +1,9 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.invitation.data +enum class ActionEnum { ACCEPT, REJECT } +data class InvitationActionModel(var action: ActionEnum, var statusCode: Int, var invitation: Invitation) diff --git a/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsModel.kt b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsModel.kt new file mode 100644 index 0000000..e260ec9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsModel.kt @@ -0,0 +1,11 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.invitation.data + +import com.nextcloud.talk.data.user.model.User + +data class InvitationsModel(var user: User, var invitations: List) diff --git a/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepository.kt b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepository.kt new file mode 100644 index 0000000..4af17c5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepository.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.invitation.data + +import com.nextcloud.talk.data.user.model.User +import io.reactivex.Observable + +interface InvitationsRepository { + fun fetchInvitations(user: User): Observable + fun acceptInvitation(user: User, invitation: Invitation): Observable + fun rejectInvitation(user: User, invitation: Invitation): Observable +} diff --git a/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt new file mode 100644 index 0000000..804b99b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt @@ -0,0 +1,72 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.invitation.data + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.utils.ApiUtils +import io.reactivex.Observable + +class InvitationsRepositoryImpl(private val ncApi: NcApi) : InvitationsRepository { + + override fun fetchInvitations(user: User): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + + return ncApi.getInvitations( + credentials, + ApiUtils.getUrlForInvitation(user.baseUrl!!) + ).map { mapToInvitationsModel(user, it.ocs?.data!!) } + } + + override fun acceptInvitation(user: User, invitation: Invitation): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + + return ncApi.acceptInvitation( + credentials, + ApiUtils.getUrlForInvitationAccept(user.baseUrl!!, invitation.id) + ).map { InvitationActionModel(ActionEnum.ACCEPT, it.ocs?.meta?.statusCode!!, invitation) } + } + + override fun rejectInvitation(user: User, invitation: Invitation): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token)!! + + return ncApi.rejectInvitation( + credentials, + ApiUtils.getUrlForInvitationReject(user.baseUrl!!, invitation.id) + ).map { InvitationActionModel(ActionEnum.REJECT, it.ocs?.meta?.statusCode!!, invitation) } + } + + private fun mapToInvitationsModel( + user: User, + invitations: List + ): InvitationsModel { + val filteredInvitations = invitations.filter { it.state == OPEN_PENDING_INVITATION } + + return InvitationsModel( + user, + filteredInvitations.map { invitation -> + Invitation( + invitation.id, + invitation.state, + invitation.localCloudId!!, + invitation.localToken!!, + invitation.remoteAttendeeId, + invitation.remoteServerUrl!!, + invitation.remoteToken!!, + invitation.roomName!!, + invitation.userId!!, + invitation.inviterCloudId!!, + invitation.inviterDisplayName!! + ) + } + ) + } + + companion object { + private const val OPEN_PENDING_INVITATION = 0 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt b/app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt new file mode 100644 index 0000000..aed1207 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt @@ -0,0 +1,121 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.invitation.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.invitation.data.ActionEnum +import com.nextcloud.talk.invitation.data.Invitation +import com.nextcloud.talk.invitation.data.InvitationActionModel +import com.nextcloud.talk.invitation.data.InvitationsModel +import com.nextcloud.talk.invitation.data.InvitationsRepository +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class InvitationsViewModel @Inject constructor(private val repository: InvitationsRepository) : ViewModel() { + + sealed interface ViewState + + object FetchInvitationsStartState : ViewState + object FetchInvitationsEmptyState : ViewState + object FetchInvitationsErrorState : ViewState + open class FetchInvitationsSuccessState(val invitations: List) : ViewState + + private val _fetchInvitationsViewState: MutableLiveData = MutableLiveData(FetchInvitationsStartState) + val fetchInvitationsViewState: LiveData + get() = _fetchInvitationsViewState + + object InvitationActionStartState : ViewState + object InvitationActionErrorState : ViewState + + private val _invitationActionViewState: MutableLiveData = MutableLiveData(InvitationActionStartState) + + open class InvitationActionSuccessState(val action: ActionEnum, val invitation: Invitation) : ViewState + + val invitationActionViewState: LiveData + get() = _invitationActionViewState + + fun fetchInvitations(user: User) { + _fetchInvitationsViewState.value = FetchInvitationsStartState + repository.fetchInvitations(user) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(FetchInvitationsObserver()) + } + + fun acceptInvitation(user: User, invitation: Invitation) { + repository.acceptInvitation(user, invitation) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(InvitationActionObserver()) + } + + fun rejectInvitation(user: User, invitation: Invitation) { + repository.rejectInvitation(user, invitation) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(InvitationActionObserver()) + } + + inner class FetchInvitationsObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(model: InvitationsModel) { + if (model.invitations.isEmpty()) { + _fetchInvitationsViewState.value = FetchInvitationsEmptyState + } else { + _fetchInvitationsViewState.value = FetchInvitationsSuccessState(model.invitations) + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching invitations") + _fetchInvitationsViewState.value = FetchInvitationsErrorState + } + + override fun onComplete() { + // unused atm + } + } + + inner class InvitationActionObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(model: InvitationActionModel) { + if (model.statusCode == HTTP_OK) { + _invitationActionViewState.value = InvitationActionSuccessState(model.action, model.invitation) + } else { + _invitationActionViewState.value = InvitationActionErrorState + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when handling invitation") + _invitationActionViewState.value = InvitationActionErrorState + } + + override fun onComplete() { + // unused atm + } + } + + companion object { + private val TAG = InvitationsViewModel::class.simpleName + private const val OPEN_PENDING_INVITATION = "0" + private const val HTTP_OK = 200 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java new file mode 100644 index 0000000..77706c1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/AccountRemovalWorker.java @@ -0,0 +1,206 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs; + +import android.app.NotificationManager; +import android.content.Context; +import android.util.Log; + +import com.nextcloud.talk.R; +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager; +import com.nextcloud.talk.data.database.dao.ChatBlocksDao; +import com.nextcloud.talk.data.database.dao.ChatMessagesDao; +import com.nextcloud.talk.data.database.dao.ConversationsDao; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.models.json.generic.GenericMeta; +import com.nextcloud.talk.models.json.generic.GenericOverall; +import com.nextcloud.talk.models.json.push.PushConfigurationState; +import com.nextcloud.talk.users.UserManager; +import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.preferences.AppPreferences; +import com.nextcloud.talk.webrtc.WebSocketConnectionHelper; + +import java.net.CookieManager; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.zip.CRC32; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import autodagger.AutoInjector; +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; +import okhttp3.JavaNetCookieJar; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +@AutoInjector(NextcloudTalkApplication.class) +public class AccountRemovalWorker extends Worker { + public static final String TAG = "AccountRemovalWorker"; + + @Inject UserManager userManager; + + @Inject ArbitraryStorageManager arbitraryStorageManager; + + @Inject AppPreferences appPreferences; + + @Inject Retrofit retrofit; + + @Inject OkHttpClient okHttpClient; + + @Inject ChatMessagesDao chatMessagesDao; + + @Inject ConversationsDao conversationsDao; + + @Inject ChatBlocksDao chatBlocksDao; + + NcApi ncApi; + + public AccountRemovalWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + Objects.requireNonNull(NextcloudTalkApplication.Companion.getSharedApplication()).getComponentApplication().inject(this); + + List users = userManager.getUsersScheduledForDeletion().blockingGet(); + for (User user : users) { + if (user.getPushConfigurationState() != null) { + PushConfigurationState finalPushConfigurationState = user.getPushConfigurationState(); + + ncApi = retrofit + .newBuilder() + .client(okHttpClient + .newBuilder() + .cookieJar(new JavaNetCookieJar(new CookieManager())) + .build()) + .build() + .create(NcApi.class); + + ncApi.unregisterDeviceForNotificationsWithNextcloud( + ApiUtils.getCredentials(user.getUsername(), user.getToken()), + ApiUtils.getUrlNextcloudPush(Objects.requireNonNull(user.getBaseUrl()))) + .blockingSubscribe(new Observer() { + @Override + public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) { + // unused atm + } + + @Override + public void onNext(@io.reactivex.annotations.NonNull GenericOverall genericOverall) { + GenericMeta meta = Objects.requireNonNull(genericOverall.getOcs()).getMeta(); + int statusCode = Objects.requireNonNull(meta).getStatusCode(); + + if (statusCode == 200 || statusCode == 202) { + HashMap queryMap = new HashMap<>(); + queryMap.put("deviceIdentifier", + finalPushConfigurationState.getDeviceIdentifier()); + queryMap.put("userPublicKey", finalPushConfigurationState.getUserPublicKey()); + queryMap.put("deviceIdentifierSignature", + finalPushConfigurationState.getDeviceIdentifierSignature()); + unregisterDeviceForNotificationWithProxy(queryMap, user); + } + } + + @Override + public void onError(@io.reactivex.annotations.NonNull Throwable e) { + Log.e(TAG, "error while trying to unregister Device For Notifications", e); + initiateUserDeletion(user); + } + + @Override + public void onComplete() { + // unused atm + } + }); + } else { + initiateUserDeletion(user); + } + } + + return Result.success(); + } + + private void unregisterDeviceForNotificationWithProxy(HashMap queryMap, User user) { + ncApi.unregisterDeviceForNotificationsWithProxy + (ApiUtils.getUrlPushProxy(), queryMap) + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + // unused atm + } + + @Override + public void onNext(Void aVoid) { + String groupName = String.format( + getApplicationContext() + .getResources() + .getString(R.string.nc_notification_channel), user.getUserId(), user.getBaseUrl()); + CRC32 crc32 = new CRC32(); + crc32.update(groupName.getBytes()); + NotificationManager notificationManager = + (NotificationManager) getApplicationContext() + .getSystemService(Context.NOTIFICATION_SERVICE); + + if (notificationManager != null) { + notificationManager.deleteNotificationChannelGroup( + Long.toString(crc32.getValue())); + } + + initiateUserDeletion(user); + } + + @Override + public void onError(Throwable e) { + Log.e(TAG, "error while trying to unregister Device For Notification With Proxy", e); + initiateUserDeletion(user); + } + + @Override + public void onComplete() { + // unused atm + } + }); + } + + private void initiateUserDeletion(User user) { + if (user.getId() != null) { + long id = user.getId(); + WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(id); + + try { + arbitraryStorageManager.deleteAllEntriesForAccountIdentifier(id); + deleteUser(user); + } catch (Throwable e) { + Log.e(TAG, "error while trying to delete All Entries For Account Identifier", e); + } + } + } + + private void deleteUser(User user) { + if (user.getId() != null) { + String username = user.getUsername(); + try { + userManager.deleteUser(user.getId()); + if (username != null) { + Log.d(TAG, "deleted user: " + username); + } + } catch (Throwable e) { + Log.e(TAG, "error while trying to delete user", e); + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversationWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversationWorker.java new file mode 100644 index 0000000..3d533f0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversationWorker.java @@ -0,0 +1,127 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs; + +import android.content.Context; + +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.models.RetrofitBucket; +import com.nextcloud.talk.users.UserManager; +import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.bundle.BundleKeys; + +import org.greenrobot.eventbus.EventBus; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import autodagger.AutoInjector; +import io.reactivex.schedulers.Schedulers; + +@AutoInjector(NextcloudTalkApplication.class) +public class AddParticipantsToConversationWorker extends Worker { + @Inject + NcApi ncApi; + + @Inject + UserManager userManager; + + @Inject + EventBus eventBus; + + public AddParticipantsToConversationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + } + + @NonNull + @Override + public Result doWork() { + Data data = getInputData(); + String[] selectedUserIds = data.getStringArray(BundleKeys.KEY_SELECTED_USERS); + String[] selectedGroupIds = data.getStringArray(BundleKeys.KEY_SELECTED_GROUPS); + String[] selectedCircleIds = data.getStringArray(BundleKeys.KEY_SELECTED_CIRCLES); + String[] selectedEmails = data.getStringArray(BundleKeys.KEY_SELECTED_EMAILS); + User user = + userManager.getUserWithInternalId( + data.getLong(BundleKeys.KEY_INTERNAL_USER_ID, -1)) + .blockingGet(); + + int apiVersion = ApiUtils.getConversationApiVersion(user, new int[] {ApiUtils.API_V4, 1}); + + String conversationToken = data.getString(BundleKeys.KEY_TOKEN); + String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken()); + + RetrofitBucket retrofitBucket; + if (selectedUserIds != null) { + for (String userId : selectedUserIds) { + retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipant(apiVersion, user.getBaseUrl(), + conversationToken, + userId); + + ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) + .subscribeOn(Schedulers.io()) + .blockingSubscribe(); + } + } + + if (selectedGroupIds != null) { + for (String groupId : selectedGroupIds) { + retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipantWithSource( + apiVersion, + user.getBaseUrl(), + conversationToken, + "groups", + groupId + ); + + ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) + .subscribeOn(Schedulers.io()) + .blockingSubscribe(); + } + } + + if (selectedCircleIds != null) { + for (String circleId : selectedCircleIds) { + retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipantWithSource( + apiVersion, + user.getBaseUrl(), + conversationToken, + "circles", + circleId + ); + + ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) + .subscribeOn(Schedulers.io()) + .blockingSubscribe(); + } + } + + if (selectedEmails != null) { + for (String email : selectedEmails) { + retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipantWithSource( + apiVersion, + user.getBaseUrl(), + conversationToken, + "emails", + email + ); + + ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) + .subscribeOn(Schedulers.io()) + .blockingSubscribe(); + } + } + + return Result.success(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesWorker.java new file mode 100644 index 0000000..08c7aca --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesWorker.java @@ -0,0 +1,157 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs; + +import android.content.Context; +import android.util.Log; + +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.events.EventStatus; +import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall; +import com.nextcloud.talk.users.UserManager; +import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.UserIdUtils; +import com.nextcloud.talk.utils.bundle.BundleKeys; + +import org.greenrobot.eventbus.EventBus; + +import java.net.CookieManager; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import autodagger.AutoInjector; +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; +import okhttp3.JavaNetCookieJar; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +@AutoInjector(NextcloudTalkApplication.class) +public class CapabilitiesWorker extends Worker { + public static final String TAG = "CapabilitiesWorker"; + public static final long NO_ID = -1; + + @Inject + UserManager userManager; + + @Inject + Retrofit retrofit; + + @Inject + EventBus eventBus; + + @Inject + OkHttpClient okHttpClient; + + NcApi ncApi; + + public CapabilitiesWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + private void updateUser(CapabilitiesOverall capabilitiesOverall, User user) { + if (capabilitiesOverall.getOcs() != null && capabilitiesOverall.getOcs().getData() != null && + capabilitiesOverall.getOcs().getData().getCapabilities() != null) { + + user.setCapabilities(capabilitiesOverall.getOcs().getData().getCapabilities()); + user.setServerVersion(capabilitiesOverall.getOcs().getData().getServerVersion()); + + try { + int rowsCount = userManager.updateOrCreateUser(user).blockingGet(); + if (rowsCount > 0) { + eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user), + EventStatus.EventType.CAPABILITIES_FETCH, + true)); + } else { + Log.w(TAG, "Error updating user"); + eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user), + EventStatus.EventType.CAPABILITIES_FETCH, + false)); + } + } catch (Exception e) { + Log.e(TAG, "Error updating user", e); + eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user), + EventStatus.EventType.CAPABILITIES_FETCH, + false)); + } + } + } + + @NonNull + @Override + public Result doWork() { + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + + Data data = getInputData(); + + long internalUserId = data.getLong(BundleKeys.KEY_INTERNAL_USER_ID, -1); + + List userEntityObjectList = new ArrayList<>(); + boolean userNotFound = userManager.getUserWithInternalId(internalUserId).isEmpty().blockingGet(); + + if (internalUserId == -1 || userNotFound) { + userEntityObjectList = userManager.getUsers().blockingGet(); + } else { + userEntityObjectList.add(userManager.getUserWithInternalId(internalUserId).blockingGet()); + } + + for (User user : userEntityObjectList) { + + ncApi = retrofit + .newBuilder() + .client(okHttpClient + .newBuilder() + .cookieJar(new JavaNetCookieJar(new CookieManager())) + .build()) + .build() + .create(NcApi.class); + + String url = ""; + String baseurl = user.getBaseUrl(); + if (baseurl != null) { + url = ApiUtils.getUrlForCapabilities(baseurl); + } + + ncApi.getCapabilities(ApiUtils.getCredentials(user.getUsername(), user.getToken()), url) + .retry(3) + .blockingSubscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + // unused atm + } + + @Override + public void onNext(CapabilitiesOverall capabilitiesOverall) { + updateUser(capabilitiesOverall, user); + } + + @Override + public void onError(Throwable e) { + eventBus.post(new EventStatus(user.getId(), + EventStatus.EventType.CAPABILITIES_FETCH, + false)); + } + + @Override + public void onComplete() { + // unused atm + } + }); + } + + return Result.success(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt new file mode 100644 index 0000000..04155bc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt @@ -0,0 +1,505 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs + +import android.Manifest +import android.accounts.Account +import android.accounts.AccountManager +import android.app.Activity +import android.content.ContentProviderOperation +import android.content.Context +import android.content.OperationApplicationException +import android.content.pm.PackageManager +import android.net.Uri +import android.os.RemoteException +import android.provider.ContactsContract +import android.util.Log +import android.widget.Toast +import androidx.core.content.ContextCompat +import androidx.core.os.ConfigurationCompat +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import autodagger.AutoInjector +import com.google.gson.Gson +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.models.json.search.ContactsByNumberOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ContactUtils +import com.nextcloud.talk.utils.DateConstants +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.preferences.AppPreferences +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ContactAddressBookWorker(val context: Context, workerParameters: WorkerParameters) : + Worker(context, workerParameters) { + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var appPreferences: AppPreferences + + private lateinit var accountName: String + private lateinit var accountType: String + + override fun doWork(): Result { + sharedApplication!!.componentApplication.inject(this) + + val currentUser = currentUserProvider.currentUser.blockingGet() + + accountName = context.getString(R.string.nc_app_product_name) + accountType = BuildConfig.APPLICATION_ID + + if (currentUser == null) { + Log.e(javaClass.simpleName, "No current user!") + return Result.failure() + } + + val deleteAll = inputData.getBoolean(DELETE_ALL, false) + if (deleteAll) { + deleteAllLinkedAccounts() + return Result.success() + } + + // Check if run already at the date + val force = inputData.getBoolean(KEY_FORCE, false) + if (!force) { + if (System.currentTimeMillis() - appPreferences.getPhoneBookIntegrationLastRun(0L) < + DateConstants.DAYS_DIVIDER * + DateConstants.HOURS_DIVIDER * + DateConstants.MINUTES_DIVIDER * + DateConstants.SECOND_DIVIDER + ) { + Log.d(TAG, "Already run within last 24h") + return Result.success() + } + } + + if (AccountManager.get(context).getAccountsByType(accountType).isEmpty()) { + AccountManager.get(context).addAccountExplicitly(Account(accountName, accountType), "", null) + } else { + Log.d(TAG, "Account already exists") + } + + val deviceContactsWithNumbers = collectContactsWithPhoneNumbersFromDevice() + + if (deviceContactsWithNumbers.isNotEmpty()) { + val currentLocale = ConfigurationCompat.getLocales(context.resources.configuration)[0]!!.country + + val map = mutableMapOf() + map["location"] = currentLocale + map["search"] = deviceContactsWithNumbers + + val json = Gson().toJson(map) + + ncApi.searchContactsByPhoneNumber( + ApiUtils.getCredentials(currentUser.username, currentUser.token), + ApiUtils.getUrlForSearchByNumber(currentUser.baseUrl!!), + json.toRequestBody("application/json".toMediaTypeOrNull()) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onComplete() { + // unused atm + } + + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(foundContacts: ContactsByNumberOverall) { + when (foundContacts.ocs?.meta?.statusCode) { + HTTP_CODE_TOO_MANY_REQUESTS -> { + Toast.makeText( + context, + context.resources.getString( + R.string.nc_settings_phone_book_integration_phone_number_dialog_429 + ), + Toast.LENGTH_SHORT + ).show() + } + else -> { + val contactsWithAssociatedPhoneNumbers = foundContacts.ocs!!.map + deleteLinkedAccounts(contactsWithAssociatedPhoneNumbers) + createLinkedAccounts(contactsWithAssociatedPhoneNumbers) + } + } + } + + override fun onError(e: Throwable) { + Log.e(javaClass.simpleName, "Failed to searchContactsByPhoneNumber", e) + } + }) + } + + // store timestamp + appPreferences.setPhoneBookIntegrationLastRun(System.currentTimeMillis()) + + return Result.success() + } + + private fun collectContactsWithPhoneNumbersFromDevice(): MutableMap> { + val deviceContactsWithNumbers: MutableMap> = mutableMapOf() + + val contactCursor = context.contentResolver.query( + ContactsContract.Contacts.CONTENT_URI, + null, + null, + null, + null + ) + + if (contactCursor != null) { + if (contactCursor.count > 0) { + contactCursor.moveToFirst() + for (i in 0 until contactCursor.count) { + val id = contactCursor.getString(contactCursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID)) + val lookup = + contactCursor.getString( + contactCursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY) + ) + deviceContactsWithNumbers[lookup] = getPhoneNumbersFromDeviceContact(id) + contactCursor.moveToNext() + } + } + contactCursor.close() + } + Log.d(TAG, "collected contacts with phonenumbers: " + deviceContactsWithNumbers.size) + return deviceContactsWithNumbers + } + + private fun deleteLinkedAccounts(contactsWithAssociatedPhoneNumbers: Map?) { + Log.d(TAG, "deleteLinkedAccount") + fun deleteLinkedAccount(id: String) { + val rawContactUri = ContactsContract.RawContacts.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .build() + val count = context.contentResolver.delete( + rawContactUri, + ContactsContract.RawContacts.CONTACT_ID + " " + "LIKE \"" + id + "\"", + null + ) + Log.d(TAG, "deleted $count linked accounts for id $id") + } + + val rawContactUri = ContactsContract.Data.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .appendQueryParameter( + ContactsContract.Data.MIMETYPE, + "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" + ) + .build() + + val rawContactsCursor = context.contentResolver.query( + rawContactUri, + null, + null, + null, + null + ) + + if (rawContactsCursor != null) { + if (rawContactsCursor.count > 0) { + while (rawContactsCursor.moveToNext()) { + val lookupKey = + rawContactsCursor.getString( + rawContactsCursor.getColumnIndexOrThrow(ContactsContract.Data.LOOKUP_KEY) + ) + val contactId = + rawContactsCursor.getString( + rawContactsCursor.getColumnIndexOrThrow(ContactsContract.Data.CONTACT_ID) + ) + + if (contactsWithAssociatedPhoneNumbers == null || + !contactsWithAssociatedPhoneNumbers.containsKey(lookupKey) + ) { + deleteLinkedAccount(contactId) + } + } + } else { + Log.d(TAG, "no contacts with linked Talk Accounts found. Nothing to delete...") + } + rawContactsCursor.close() + } + } + + private fun createLinkedAccounts(contactsWithAssociatedPhoneNumbers: Map?) { + fun hasLinkedAccount(id: String): Boolean { + var hasLinkedAccount = false + val where = + ContactsContract.Data.MIMETYPE + + " = ? AND " + + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + + " = ?" + val params = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id) + + val rawContactUri = ContactsContract.Data.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .appendQueryParameter( + ContactsContract.Data.MIMETYPE, + "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" + ) + .build() + + val rawContactsCursor = context.contentResolver.query( + rawContactUri, + null, + where, + params, + null + ) + + if (rawContactsCursor != null) { + if (rawContactsCursor.count > 0) { + hasLinkedAccount = true + Log.d(TAG, "contact with id $id already has a linked account") + } else { + hasLinkedAccount = false + } + rawContactsCursor.close() + } + return hasLinkedAccount + } + + fun createOps( + cloudId: String, + numbers: MutableList, + displayName: String? + ): ArrayList { + val ops = ArrayList() + val rawContactsUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon().build() + val dataUri = ContactsContract.Data.CONTENT_URI.buildUpon().build() + + ops.add( + ContentProviderOperation + .newInsert(rawContactsUri) + .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .withValue( + ContactsContract.RawContacts.AGGREGATION_MODE, + ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT + ) + .withValue(ContactsContract.RawContacts.SYNC2, cloudId) + .build() + ) + ops.add( + ContentProviderOperation + .newInsert(dataUri) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + ) + .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, numbers[0]) + .build() + ) + ops.add( + ContentProviderOperation + .newInsert(dataUri) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + ) + .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName) + .build() + ) + ops.add( + ContentProviderOperation + .newInsert(dataUri) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue( + ContactsContract.Data.MIMETYPE, + "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" + ) + .withValue(ContactsContract.Data.DATA1, cloudId) + .withValue( + ContactsContract.Data.DATA2, + String.format( + context.resources.getString(R.string.nc_phone_book_integration_chat_via), + accountName + ) + ) + .build() + ) + return ops + } + + fun createLinkedAccount(lookupKey: String, cloudId: String) { + val lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey) + val lookupContactUri = ContactsContract.Contacts.lookupContact(context.contentResolver, lookupUri) + val contactCursor = context.contentResolver.query(lookupContactUri, null, null, null, null) + + if (contactCursor != null) { + if (contactCursor.count > 0) { + contactCursor.moveToFirst() + + val id = contactCursor.getString(contactCursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID)) + if (hasLinkedAccount(id)) { + return + } + + val numbers = getPhoneNumbersFromDeviceContact(id) + val displayName = ContactUtils.getDisplayNameFromDeviceContact(context, id) + + if (displayName == null) { + return + } + + val ops = createOps(cloudId, numbers, displayName) + + try { + context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) + } catch (e: OperationApplicationException) { + Log.e(TAG, "", e) + } catch (e: RemoteException) { + Log.e(TAG, "", e) + } + + Log.d( + TAG, + "added new entry for contact $displayName (cloudId: $cloudId | lookupKey: $lookupKey" + + " | id: $id)" + ) + } + contactCursor.close() + } + } + + if (contactsWithAssociatedPhoneNumbers != null && contactsWithAssociatedPhoneNumbers.isNotEmpty()) { + for (contact in contactsWithAssociatedPhoneNumbers) { + val lookupKey = contact.key + val cloudId = contact.value + createLinkedAccount(lookupKey, cloudId) + } + } else { + Log.d(TAG, "no contacts with linked Talk Accounts found. No linked accounts created.") + } + } + + private fun getPhoneNumbersFromDeviceContact(id: String?): MutableList { + val numbers = mutableListOf() + val phonesNumbersCursor = context.contentResolver.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + null, + ContactsContract.Data.CONTACT_ID + " = " + id, + null, + null + ) + + if (phonesNumbersCursor != null) { + while (phonesNumbersCursor.moveToNext()) { + numbers.add( + phonesNumbersCursor.getString( + phonesNumbersCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER) + ) + ) + } + phonesNumbersCursor.close() + } + if (numbers.size > 0) { + Log.d(TAG, "Found ${numbers.size} phone numbers for contact with id $id") + } + return numbers + } + + fun deleteAllLinkedAccounts() { + val rawContactUri = ContactsContract.RawContacts.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName) + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType) + .build() + context.contentResolver.delete(rawContactUri, null, null) + Log.d(TAG, "deleted all linked accounts") + } + + companion object { + const val TAG = "ContactAddressBook" + const val REQUEST_PERMISSION = 231 + const val KEY_FORCE = "KEY_FORCE" + const val DELETE_ALL = "DELETE_ALL" + private const val HTTP_CODE_TOO_MANY_REQUESTS: Int = 429 + + fun run(context: Context) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) == + PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == + PackageManager.PERMISSION_GRANTED + ) { + WorkManager.getInstance().enqueue( + OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) + .setInputData(Data.Builder().putBoolean(KEY_FORCE, false).build()).build() + ) + } + } + + fun checkPermission(activity: Activity, context: Context): Boolean { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != + PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) != + PackageManager.PERMISSION_GRANTED + ) { + activity.requestPermissions( + arrayOf(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS), + REQUEST_PERMISSION + ) + return false + } else { + WorkManager + .getInstance() + .enqueue( + OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) + .setInputData(Data.Builder().putBoolean(KEY_FORCE, true).build()) + .build() + ) + return true + } + } + + fun deleteAll() { + WorkManager + .getInstance() + .enqueue( + OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java) + .setInputData(Data.Builder().putBoolean(DELETE_ALL, true).build()) + .build() + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java new file mode 100644 index 0000000..e664a2f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java @@ -0,0 +1,113 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs; + +import android.content.Context; + +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.events.EventStatus; +import com.nextcloud.talk.models.json.generic.GenericOverall; +import com.nextcloud.talk.users.UserManager; +import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.UserIdUtils; +import com.nextcloud.talk.utils.bundle.BundleKeys; + +import org.greenrobot.eventbus.EventBus; + +import java.net.CookieManager; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import autodagger.AutoInjector; +import io.reactivex.Observer; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import okhttp3.JavaNetCookieJar; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +@AutoInjector(NextcloudTalkApplication.class) +public class DeleteConversationWorker extends Worker { + @Inject + Retrofit retrofit; + + @Inject + OkHttpClient okHttpClient; + + @Inject + UserManager userManager; + + @Inject + EventBus eventBus; + + NcApi ncApi; + + public DeleteConversationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + } + + @NonNull + @Override + public Result doWork() { + Data data = getInputData(); + long operationUserId = data.getLong(BundleKeys.KEY_INTERNAL_USER_ID, -1); + String conversationToken = data.getString(BundleKeys.KEY_ROOM_TOKEN); + User operationUser = userManager.getUserWithId(operationUserId).blockingGet(); + + if (operationUser != null) { + int apiVersion = ApiUtils.getConversationApiVersion(operationUser, new int[]{ApiUtils.API_V4, 1}); + + String credentials = ApiUtils.getCredentials(operationUser.getUsername(), operationUser.getToken()); + ncApi = retrofit + .newBuilder() + .client(okHttpClient.newBuilder().cookieJar(new JavaNetCookieJar(new CookieManager())).build()) + .build() + .create(NcApi.class); + + EventStatus eventStatus = new EventStatus(UserIdUtils.INSTANCE.getIdForUser(operationUser), + EventStatus.EventType.CONVERSATION_UPDATE, + true); + + ncApi.deleteRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, operationUser.getBaseUrl(), + conversationToken)) + .subscribeOn(Schedulers.io()) + .blockingSubscribe(new Observer() { + Disposable disposable; + + @Override + public void onSubscribe(Disposable d) { + disposable = d; + } + + @Override + public void onNext(GenericOverall genericOverall) { + eventBus.postSticky(eventStatus); + } + + @Override + public void onError(Throwable e) { + // unused atm + } + + @Override + public void onComplete() { + disposable.dispose(); + } + }); + } + + return Result.success(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt new file mode 100644 index 0000000..0a70434 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/DownloadFileToCacheWorker.kt @@ -0,0 +1,156 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs + +import android.content.Context +import android.util.Log +import androidx.work.Data +import androidx.work.Worker +import androidx.work.WorkerParameters +import autodagger.AutoInjector +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.preferences.AppPreferences +import okhttp3.ResponseBody +import java.io.BufferedInputStream +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerParameters) : + Worker(context, workerParameters) { + + private var totalFileSize: Long = -1 + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var appPreferences: AppPreferences + + override fun doWork(): Result { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + if (totalFileSize > -1) { + setProgressAsync(Data.Builder().putInt(PROGRESS, 0).build()) + } + + try { + val currentUser = currentUserProvider.currentUser.blockingGet() + val baseUrl = inputData.getString(KEY_BASE_URL) + val userId = inputData.getString(KEY_USER_ID) + val attachmentFolder = inputData.getString(KEY_ATTACHMENT_FOLDER) + val fileName = inputData.getString(KEY_FILE_NAME) + val remotePath = inputData.getString(KEY_FILE_PATH) + totalFileSize = (inputData.getLong(KEY_FILE_SIZE, -1)) + + checkNotNull(currentUser) + checkNotNull(baseUrl) + checkNotNull(userId) + checkNotNull(attachmentFolder) + checkNotNull(fileName) + checkNotNull(remotePath) + + val url = ApiUtils.getUrlForFileDownload(baseUrl, userId, remotePath) + + return downloadFile(currentUser, url, fileName) + } catch (e: IllegalStateException) { + Log.e(javaClass.simpleName, "Something went wrong when trying to download file", e) + return Result.failure() + } + } + + private fun downloadFile(currentUser: User, url: String, fileName: String): Result { + val downloadCall = ncApi.downloadFile( + ApiUtils.getCredentials(currentUser.username, currentUser.token), + url + ) + + return executeDownload(downloadCall.execute().body(), fileName) + } + + private fun executeDownload(body: ResponseBody?, fileName: String): Result { + if (body == null) { + Log.e(TAG, "Response body when downloading $fileName is null!") + return Result.failure() + } + + var count: Int + val data = ByteArray(BYTE_UNIT_DIVIDER * DATA_BYTES) + val bis: InputStream = BufferedInputStream(body.byteStream(), BYTE_UNIT_DIVIDER * DOWNLOAD_STREAM_SIZE) + val outputFile = File(context.cacheDir, fileName + "_") + val output: OutputStream = FileOutputStream(outputFile) + var total: Long = 0 + val startTime = System.currentTimeMillis() + var timeCount = 1 + + count = bis.read(data) + + while (count != -1) { + if (totalFileSize > -1) { + total += count.toLong() + val progress = (total * COMPLETE_PERCENTAGE / totalFileSize).toInt() + val currentTime = System.currentTimeMillis() - startTime + if (currentTime > PROGRESS_THRESHOLD * timeCount) { + setProgressAsync(Data.Builder().putInt(PROGRESS, progress).build()) + timeCount++ + } + } + output.write(data, 0, count) + count = bis.read(data) + } + + output.flush() + output.close() + bis.close() + + return onDownloadComplete(fileName) + } + + private fun onDownloadComplete(fileName: String): Result { + val tempFile = File(context.cacheDir, fileName + "_") + val targetFile = File(context.cacheDir, fileName) + + return if (tempFile.renameTo(targetFile)) { + setProgressAsync(Data.Builder().putBoolean(SUCCESS, true).build()) + Result.success() + } else { + Result.failure() + } + } + + companion object { + const val TAG = "DownloadFileToCache" + const val KEY_BASE_URL = "KEY_BASE_URL" + const val KEY_USER_ID = "KEY_USER_ID" + const val KEY_ATTACHMENT_FOLDER = "KEY_ATTACHMENT_FOLDER" + const val KEY_FILE_NAME = "KEY_FILE_NAME" + const val KEY_FILE_PATH = "KEY_FILE_PATH" + const val KEY_FILE_SIZE = "KEY_FILE_SIZE" + const val PROGRESS = "PROGRESS" + const val SUCCESS = "SUCCESS" + const val BYTE_UNIT_DIVIDER = 1024 + const val DATA_BYTES = 4 + const val DOWNLOAD_STREAM_SIZE = 8 + const val COMPLETE_PERCENTAGE = 100 + const val PROGRESS_THRESHOLD = 50 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt new file mode 100644 index 0000000..d985c7a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.kt @@ -0,0 +1,108 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import androidx.work.Data +import androidx.work.ListenableWorker +import androidx.work.WorkerParameters +import androidx.work.impl.utils.futures.SettableFuture +import autodagger.AutoInjector +import com.google.common.util.concurrent.ListenableFuture +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ApiUtils.getConversationApiVersion +import com.nextcloud.talk.utils.ApiUtils.getCredentials +import com.nextcloud.talk.utils.ApiUtils.getUrlForParticipantsSelf +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import retrofit2.HttpException +import javax.inject.Inject + +@SuppressLint("RestrictedApi") +@AutoInjector(NextcloudTalkApplication::class) +class LeaveConversationWorker(context: Context, workerParams: WorkerParameters) : + ListenableWorker(context, workerParams) { + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + private val result = SettableFuture.create() + + override fun startWork(): ListenableFuture { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + val conversationToken = inputData.getString(BundleKeys.KEY_ROOM_TOKEN) + val currentUser = currentUserProvider.currentUser.blockingGet() + + if (currentUser != null && conversationToken != null) { + val credentials = getCredentials(currentUser.username, currentUser.token) + val apiVersion = getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, 1)) + + ncApi.removeSelfFromRoom( + credentials, + getUrlForParticipantsSelf(apiVersion, currentUser.baseUrl, conversationToken) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(p0: GenericOverall) { + // unused atm + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to remove self from room", e) + val httpException = e as? HttpException + val errorData = if (httpException?.code() == HTTP_ERROR_CODE_400) { + Data.Builder() + .putString("error_type", ERROR_NO_OTHER_MODERATORS_OR_OWNERS_LEFT) + .build() + } else { + Data.Builder() + .putString("error_type", ERROR_OTHER) + .build() + } + result.set(Result.failure(errorData)) + } + + override fun onComplete() { + result.set(Result.success()) + } + }) + } else { + result.set(Result.failure()) + } + + return result + } + + companion object { + private const val TAG = "LeaveConversationWorker" + const val ERROR_NO_OTHER_MODERATORS_OR_OWNERS_LEFT = "NO_OTHER_MODERATORS_OR_OWNERS_LEFT" + const val ERROR_OTHER = "ERROR_OTHER" + const val HTTP_ERROR_CODE_400 = 400 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt new file mode 100644 index 0000000..df26436 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -0,0 +1,1018 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2025 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs + +import android.Manifest +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.service.notification.StatusBarNotification +import android.text.TextUtils +import android.util.Base64 +import android.util.Log +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.Person +import androidx.core.app.RemoteInput +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.core.net.toUri +import androidx.emoji2.text.EmojiCompat +import androidx.work.Data +import androidx.work.Worker +import androidx.work.WorkerParameters +import autodagger.AutoInjector +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.MainActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.callnotification.CallNotificationActivity +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.models.SignatureVerification +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.notifications.NotificationOverall +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.models.json.participants.ParticipantsOverall +import com.nextcloud.talk.models.json.push.DecryptedPushMessage +import com.nextcloud.talk.models.json.push.NotificationUser +import com.nextcloud.talk.receivers.DirectReplyReceiver +import com.nextcloud.talk.receivers.DismissRecordingAvailableReceiver +import com.nextcloud.talk.receivers.MarkAsReadReceiver +import com.nextcloud.talk.receivers.ShareRecordingToChatReceiver +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.NotificationUtils.cancelAllNotificationsForAccount +import com.nextcloud.talk.utils.NotificationUtils.cancelNotification +import com.nextcloud.talk.utils.NotificationUtils.findNotificationForRoom +import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri +import com.nextcloud.talk.utils.NotificationUtils.loadAvatarSync +import com.nextcloud.talk.utils.ParticipantPermissions +import com.nextcloud.talk.utils.PushUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_DISMISS_RECORDING_URL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MESSAGE_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_RESTRICT_DELETION +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_TIMESTAMP +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_REMOTE_TALK_SHARE +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ONE_TO_ONE +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID +import com.nextcloud.talk.utils.preferences.AppPreferences +import io.reactivex.Observable +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import okhttp3.JavaNetCookieJar +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import java.net.CookieManager +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import java.security.PrivateKey +import java.util.concurrent.TimeUnit +import java.util.function.Consumer +import java.util.zip.CRC32 +import javax.crypto.Cipher +import javax.crypto.NoSuchPaddingException +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class NotificationWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { + + @Inject + lateinit var appPreferences: AppPreferences + + @JvmField + @Inject + var arbitraryStorageManager: ArbitraryStorageManager? = null + + @JvmField + @Inject + var retrofit: Retrofit? = null + + var chatNetworkDataSource: ChatNetworkDataSource? = null + @Inject set + + @Inject + lateinit var userManager: UserManager + + @JvmField + @Inject + var okHttpClient: OkHttpClient? = null + private lateinit var credentials: String + private lateinit var ncApi: NcApi + private lateinit var pushMessage: DecryptedPushMessage + private lateinit var signatureVerification: SignatureVerification + private var context: Context? = null + private var conversationType: String? = "one2one" + private lateinit var notificationManager: NotificationManagerCompat + + override fun doWork(): Result { + sharedApplication!!.componentApplication.inject(this) + context = applicationContext + + initDecryptedData(inputData) + initNcApiAndCredentials() + + notificationManager = NotificationManagerCompat.from(context!!) + + pushMessage.timestamp = System.currentTimeMillis() + + Log.d(TAG, pushMessage.toString()) + Log.d(TAG, "pushMessage.id (=KEY_ROOM_TOKEN): " + pushMessage.id) + Log.d(TAG, "pushMessage.notificationId: " + pushMessage.notificationId) + Log.d(TAG, "pushMessage.notificationIds: " + pushMessage.notificationIds) + Log.d(TAG, "pushMessage.timestamp: " + pushMessage.timestamp) + + if (pushMessage.delete) { + cancelNotification(context, signatureVerification.user!!, pushMessage.notificationId) + } else if (pushMessage.deleteAll) { + cancelAllNotificationsForAccount(context, signatureVerification.user!!) + } else if (pushMessage.deleteMultiple) { + for (notificationId in pushMessage.notificationIds!!) { + cancelNotification(context, signatureVerification.user!!, notificationId) + } + } else if (isTalkNotification()) { + Log.d(TAG, "pushMessage.type: " + pushMessage.type) + when (pushMessage.type) { + TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING, TYPE_REMINDER -> handleNonCallPushMessage() + TYPE_REMOTE_TALK_SHARE -> handleRemoteTalkSharePushMessage() + TYPE_CALL -> handleCallPushMessage() + else -> Log.e(TAG, pushMessage.type + " is not handled") + } + } else if (isAdminTalkNotification()) { + Log.d(TAG, "pushMessage.type: " + pushMessage.type) + when (pushMessage.type) { + TYPE_ADMIN_NOTIFICATIONS -> handleTestPushMessage() + else -> Log.e(TAG, pushMessage.type + " is not handled") + } + } else { + Log.d(TAG, "a pushMessage that is not for spreed was received.") + } + + return Result.success() + } + + private fun handleTestPushMessage() { + val intent = Intent(context, MainActivity::class.java) + intent.flags = getIntentFlags() + showNotification(intent, null) + } + + private fun handleNonCallPushMessage() { + val mainActivityIntent = createMainActivityIntent() + if (pushMessage.notificationId != Long.MIN_VALUE) { + getNcDataAndShowNotification(mainActivityIntent) + } else { + showNotification(mainActivityIntent, null) + } + } + + private fun handleRemoteTalkSharePushMessage() { + val mainActivityIntent = Intent(context, MainActivity::class.java) + mainActivityIntent.flags = getIntentFlags() + val bundle = Bundle() + bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!) + bundle.putBoolean(KEY_REMOTE_TALK_SHARE, true) + mainActivityIntent.putExtras(bundle) + + if (pushMessage.notificationId != Long.MIN_VALUE) { + getNcDataAndShowNotification(mainActivityIntent) + } else { + showNotification(mainActivityIntent, null) + } + } + + private fun handleCallPushMessage() { + val userBeingCalled = userManager.getUserWithId(signatureVerification.user!!.id!!).blockingGet() + + fun createBundle(conversation: ConversationModel): Bundle { + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, pushMessage.id) + bundle.putInt(KEY_NOTIFICATION_TIMESTAMP, pushMessage.timestamp.toInt()) + bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!) + bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, true) + + val isOneToOneCall = conversation.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + + bundle.putBoolean(KEY_ROOM_ONE_TO_ONE, isOneToOneCall) // ggf change in Activity? not necessary???? + bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, conversation.name) + bundle.putString(BundleKeys.KEY_CONVERSATION_DISPLAY_NAME, conversation.displayName) + bundle.putInt(BundleKeys.KEY_CALL_FLAG, conversation.callFlag) + + val participantPermission = ParticipantPermissions( + userBeingCalled!!.capabilities!!.spreedCapability!!, + conversation + ) + bundle.putBoolean( + BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO, + participantPermission.canPublishAudio() + ) + bundle.putBoolean( + BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO, + participantPermission.canPublishVideo() + ) + bundle.putBoolean( + BundleKeys.KEY_IS_MODERATOR, + ConversationUtils.isParticipantOwnerOrModerator(conversation) + ) + return bundle + } + + fun prepareCallNotificationScreen(conversation: ConversationModel) { + val fullScreenIntent = Intent(context, CallNotificationActivity::class.java) + val bundle = createBundle(conversation) + + fullScreenIntent.putExtras(bundle) + fullScreenIntent.flags = getIntentFlags() + + val requestCode = System.currentTimeMillis().toInt() + + val fullScreenPendingIntent = PendingIntent.getActivity( + context, + requestCode, + fullScreenIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + ) + + val soundUri = getCallRingtoneUri(applicationContext, appPreferences) + val notificationChannelId = NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name + val uri = signatureVerification.user!!.baseUrl!!.toUri() + val baseUrl = uri.host + + val notification = + NotificationCompat.Builder(applicationContext, notificationChannelId) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setSmallIcon(R.drawable.ic_call_black_24dp) + .setSubText(baseUrl) + .setShowWhen(true) + .setWhen(pushMessage.timestamp) + .setContentTitle(EmojiCompat.get().process(pushMessage.subject)) + // auto cancel is set to false because notification (including sound) should continue while + // CallNotificationActivity is active + .setAutoCancel(false) + .setOngoing(true) + .setContentIntent(fullScreenPendingIntent) + .setFullScreenIntent(fullScreenPendingIntent, true) + .setSound(soundUri) + .build() + notification.flags = notification.flags or Notification.FLAG_INSISTENT + + sendNotification(pushMessage.timestamp.toInt(), notification) + + checkIfCallIsActive(signatureVerification, conversation) + } + + chatNetworkDataSource?.getRoom(userBeingCalled, roomToken = pushMessage.id!!) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversation: ConversationModel) { + if (userManager.setUserAsActive(userBeingCalled!!).blockingGet()) { + prepareCallNotificationScreen(conversation) + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to get room", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun initNcApiAndCredentials() { + credentials = ApiUtils.getCredentials( + signatureVerification.user!!.username, + signatureVerification.user!!.token + )!! + ncApi = retrofit!!.newBuilder().client( + okHttpClient!!.newBuilder().cookieJar( + JavaNetCookieJar( + CookieManager() + ) + ).build() + ).build().create( + NcApi::class.java + ) + } + + @Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "ComplexMethod", "LongMethod") + private fun initDecryptedData(inputData: Data) { + val subject = inputData.getString(BundleKeys.KEY_NOTIFICATION_SUBJECT) + val signature = inputData.getString(BundleKeys.KEY_NOTIFICATION_SIGNATURE) + try { + val base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT) + val base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT) + val pushUtils = PushUtils() + val privateKey = pushUtils.readKeyFromFile(false) as PrivateKey + try { + signatureVerification = pushUtils.verifySignature( + base64DecodedSignature, + base64DecodedSubject + ) + if (signatureVerification.signatureValid) { + val cipher = Cipher.getInstance("RSA/None/PKCS1Padding") + cipher.init(Cipher.DECRYPT_MODE, privateKey) + val decryptedSubject = cipher.doFinal(base64DecodedSubject) + + pushMessage = LoganSquare.parse( + String(decryptedSubject), + DecryptedPushMessage::class.java + ) + } + } catch (e: NoSuchAlgorithmException) { + Log.e(TAG, "No proper algorithm to decrypt the message ", e) + } catch (e: NoSuchPaddingException) { + Log.e(TAG, "No proper padding to decrypt the message ", e) + } catch (e: InvalidKeyException) { + Log.e(TAG, "Invalid private key ", e) + } + } catch (e: Exception) { + Log.e(TAG, "Error occurred while initializing decoded data ", e) + } + } + + private fun isTalkNotification() = SPREED_APP == pushMessage.app + + private fun isAdminTalkNotification() = ADMIN_NOTIFICATION_TALK == pushMessage.app + + private fun getNcDataAndShowNotification(intent: Intent) { + val user = signatureVerification.user + + // see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md + ncApi.getNcNotification( + credentials, + ApiUtils.getUrlForNcNotificationWithId( + user!!.baseUrl!!, + (pushMessage.notificationId!!).toString() + ) + ) + .blockingSubscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(notificationOverall: NotificationOverall) { + val ncNotification = notificationOverall.ocs!!.notification + if (ncNotification != null) { + enrichPushMessageByNcNotificationData(ncNotification) + showNotification(intent, ncNotification) + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to get NC notification", e) + if (BuildConfig.DEBUG) { + Handler(Looper.getMainLooper()).post { + Toast.makeText(context, "Failed to get NC notification", Toast.LENGTH_LONG).show() + } + } + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun enrichPushMessageByNcNotificationData( + ncNotification: com.nextcloud.talk.models.json.notifications.Notification + ) { + pushMessage.objectId = ncNotification.objectId + pushMessage.timestamp = ncNotification.datetime!!.millis + + if (ncNotification.messageRichParameters != null && + ncNotification.messageRichParameters!!.isNotEmpty() + ) { + pushMessage.text = getParsedMessage( + ncNotification.messageRich, + ncNotification.messageRichParameters + ) + } else { + pushMessage.text = ncNotification.message + } + + val subjectRichParameters = ncNotification.subjectRichParameters + if (subjectRichParameters != null && subjectRichParameters.isNotEmpty()) { + val callHashMap = subjectRichParameters["call"] + val userHashMap = subjectRichParameters["user"] + val guestHashMap = subjectRichParameters["guest"] + if (callHashMap != null && callHashMap.isNotEmpty() && callHashMap.containsKey("name")) { + if (subjectRichParameters.containsKey("reaction")) { + pushMessage.subject = "" + } else if (ncNotification.objectType == "chat") { + pushMessage.subject = callHashMap["name"]!! + } else { + pushMessage.subject = ncNotification.subject!! + } + + if (subjectRichParameters.containsKey("reaction")) { + pushMessage.text = ncNotification.subject + } + + if (callHashMap.containsKey("call-type")) { + conversationType = callHashMap["call-type"] + } + } + val notificationUser = NotificationUser() + if (userHashMap != null && userHashMap.isNotEmpty()) { + notificationUser.id = userHashMap["id"] + notificationUser.type = userHashMap["type"] + notificationUser.name = userHashMap["name"] + pushMessage.notificationUser = notificationUser + } else if (guestHashMap != null && guestHashMap.isNotEmpty()) { + notificationUser.id = guestHashMap["id"] + notificationUser.type = guestHashMap["type"] + notificationUser.name = guestHashMap["name"] + pushMessage.notificationUser = notificationUser + } + } else { + pushMessage.subject = ncNotification.subject.orEmpty() + } + } + + @Suppress("MagicNumber") + private fun showNotification( + intent: Intent, + ncNotification: com.nextcloud.talk.models.json.notifications.Notification? + ) { + var category = "" + when (pushMessage.type) { + TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING, TYPE_REMINDER, TYPE_ADMIN_NOTIFICATIONS, TYPE_REMOTE_TALK_SHARE -> + category = Notification.CATEGORY_MESSAGE + + TYPE_CALL -> + category = Notification.CATEGORY_CALL + + else -> Log.e(TAG, "unknown pushMessage.type") + } + + val pendingIntent = createUniquePendingIntent(intent) + val uri = signatureVerification.user!!.baseUrl!!.toUri() + val baseUrl = uri.host + + var contentTitle: CharSequence? = "" + if (!TextUtils.isEmpty(pushMessage.subject)) { + contentTitle = EmojiCompat.get().process(pushMessage.subject) + } + + var contentText: CharSequence? = "" + if (!TextUtils.isEmpty(pushMessage.text)) { + contentText = EmojiCompat.get().process(pushMessage.text!!) + } + + val autoCancelOnClick = TYPE_RECORDING != pushMessage.type + + val notificationBuilder = + createNotificationBuilder(category, contentTitle, contentText, baseUrl, pendingIntent, autoCancelOnClick) + val activeStatusBarNotification = findNotificationForRoom( + context, + signatureVerification.user!!, + pushMessage.id!! + ) + + // NOTE - systemNotificationId is an internal ID used on the device only. + // It is NOT the same as the notification ID used in communication with the server. + val systemNotificationId: Int = + activeStatusBarNotification?.id ?: calculateCRC32(System.currentTimeMillis().toString()).toInt() + + if ((TYPE_CHAT == pushMessage.type || TYPE_REMINDER == pushMessage.type) && + pushMessage.notificationUser != null + ) { + prepareChatNotification(notificationBuilder, activeStatusBarNotification) + addReplyAction(notificationBuilder, systemNotificationId) + addMarkAsReadAction(notificationBuilder, systemNotificationId) + } + + if (TYPE_RECORDING == pushMessage.type && ncNotification != null) { + addDismissRecordingAvailableAction(notificationBuilder, systemNotificationId, ncNotification) + addShareRecordingToChatAction(notificationBuilder, systemNotificationId, ncNotification) + } + sendNotification(systemNotificationId, notificationBuilder.build()) + } + + private fun createNotificationBuilder( + category: String, + contentTitle: CharSequence?, + contentText: CharSequence?, + baseUrl: String?, + pendingIntent: PendingIntent?, + autoCancelOnClick: Boolean + ): NotificationCompat.Builder { + val notificationBuilder = NotificationCompat.Builder(context!!, "1") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(category) + .setLargeIcon(getLargeIcon()) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSubText(baseUrl) + .setWhen(pushMessage.timestamp) + .setShowWhen(true) + .setContentIntent(pendingIntent) + .setAutoCancel(autoCancelOnClick) + .setColor(context!!.resources.getColor(R.color.colorPrimary, null)) + + val notificationInfoBundle = Bundle() + notificationInfoBundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!) + // could be an ID or a TOKEN + notificationInfoBundle.putString(KEY_ROOM_TOKEN, pushMessage.id) + notificationInfoBundle.putLong(KEY_NOTIFICATION_ID, pushMessage.notificationId!!) + + if (pushMessage.type == TYPE_RECORDING) { + notificationInfoBundle.putBoolean(KEY_NOTIFICATION_RESTRICT_DELETION, true) + } + + notificationBuilder.setExtras(notificationInfoBundle) + + when (pushMessage.type) { + TYPE_CHAT, + TYPE_ROOM, + TYPE_RECORDING, + TYPE_REMINDER, + TYPE_ADMIN_NOTIFICATIONS, + TYPE_REMOTE_TALK_SHARE -> { + notificationBuilder.setChannelId( + NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name + ) + } + } + + notificationBuilder.setContentIntent(pendingIntent) + val groupName = signatureVerification.user!!.id.toString() + "@" + pushMessage.id + notificationBuilder.setGroup(calculateCRC32(groupName).toString()) + return notificationBuilder + } + + private fun getLargeIcon(): Bitmap { + val largeIcon: Bitmap + if (pushMessage.type == TYPE_RECORDING) { + largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_baseline_videocam_24)?.toBitmap()!! + } else { + when (conversationType) { + "one2one" -> { + pushMessage.subject = "" + largeIcon = + ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!! + } + + "group" -> + largeIcon = + ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!! + + "public" -> + largeIcon = + ContextCompat.getDrawable(context!!, R.drawable.ic_link_black_24px)?.toBitmap()!! + + else -> // assuming one2one + largeIcon = if (TYPE_CHAT == pushMessage.type || TYPE_ROOM == pushMessage.type) { + ContextCompat.getDrawable(context!!, R.drawable.ic_comment)?.toBitmap()!! + } else if (TYPE_REMINDER == pushMessage.type) { + ContextCompat.getDrawable(context!!, R.drawable.ic_timer_black_24dp)?.toBitmap()!! + } else { + ContextCompat.getDrawable(context!!, R.drawable.ic_call_black_24dp)?.toBitmap()!! + } + } + } + return largeIcon + } + + private fun calculateCRC32(s: String): Long { + val crc32 = CRC32() + crc32.update(s.toByteArray()) + return crc32.value + } + + private fun prepareChatNotification( + notificationBuilder: NotificationCompat.Builder, + activeStatusBarNotification: StatusBarNotification? + ) { + val notificationUser = pushMessage.notificationUser + val userType = notificationUser!!.type + var style: NotificationCompat.MessagingStyle? = null + if (activeStatusBarNotification != null) { + style = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification( + activeStatusBarNotification.notification + ) + } + val person = Person.Builder() + .setKey(signatureVerification.user!!.id.toString() + "@" + notificationUser.id) + .setName(EmojiCompat.get().process(notificationUser.name!!)) + .setBot("bot" == userType) + notificationBuilder.setOnlyAlertOnce(false) + + if ("user" == userType || "guest" == userType) { + val baseUrl = signatureVerification.user!!.baseUrl + val avatarUrl = if ("user" == userType) { + ApiUtils.getUrlForAvatar( + baseUrl!!, + notificationUser.id, + false + ) + } else { + ApiUtils.getUrlForGuestAvatar(baseUrl!!, notificationUser.name, false) + } + person.setIcon(loadAvatarSync(avatarUrl, context!!)) + } + notificationBuilder.setStyle(getStyle(person.build(), style)) + } + + private fun buildIntentForAction(cls: Class<*>, systemNotificationId: Int, messageId: Int): PendingIntent { + val actualIntent = Intent(context, cls) + + // NOTE - systemNotificationId is an internal ID used on the device only. + // It is NOT the same as the notification ID used in communication with the server. + actualIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId) + actualIntent.putExtra(KEY_INTERNAL_USER_ID, signatureVerification.user?.id) + actualIntent.putExtra(KEY_ROOM_TOKEN, pushMessage.id) + actualIntent.putExtra(KEY_MESSAGE_ID, messageId) + + val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + return PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, intentFlag) + } + + private fun addMarkAsReadAction(notificationBuilder: NotificationCompat.Builder, systemNotificationId: Int) { + if (pushMessage.objectId != null) { + val messageId: Int = try { + parseMessageId(pushMessage.objectId!!) + } catch (nfe: NumberFormatException) { + Log.e(TAG, "Failed to parse messageId from objectId, skip adding mark-as-read action.", nfe) + return + } + + val pendingIntent = buildIntentForAction( + MarkAsReadReceiver::class.java, + systemNotificationId, + messageId + ) + val markAsReadAction = NotificationCompat.Action.Builder( + R.drawable.ic_eye, + context!!.resources.getString(R.string.nc_mark_as_read), + pendingIntent + ) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .setShowsUserInterface(false) + .build() + notificationBuilder.addAction(markAsReadAction) + } + } + + private fun addReplyAction(notificationBuilder: NotificationCompat.Builder, systemNotificationId: Int) { + val replyLabel = context!!.resources.getString(R.string.nc_reply) + val remoteInput = RemoteInput.Builder(NotificationUtils.KEY_DIRECT_REPLY) + .setLabel(replyLabel) + .build() + + val replyPendingIntent = buildIntentForAction( + DirectReplyReceiver::class.java, + systemNotificationId, + 0 + ) + val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .setShowsUserInterface(false) + .setAllowGeneratedReplies(true) + .addRemoteInput(remoteInput) + .build() + notificationBuilder.addAction(replyAction) + } + + private fun addDismissRecordingAvailableAction( + notificationBuilder: NotificationCompat.Builder, + systemNotificationId: Int, + ncNotification: com.nextcloud.talk.models.json.notifications.Notification + ) { + var dismissLabel = "" + var dismissRecordingUrl = "" + + for (action in ncNotification.actions!!) { + if (!action.primary) { + dismissLabel = action.label.orEmpty() + dismissRecordingUrl = action.link.orEmpty() + } + } + + val dismissIntent = Intent(context, DismissRecordingAvailableReceiver::class.java) + dismissIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId) + dismissIntent.putExtra(KEY_DISMISS_RECORDING_URL, dismissRecordingUrl) + + val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + val dismissPendingIntent = PendingIntent.getBroadcast(context, systemNotificationId, dismissIntent, intentFlag) + + val dismissAction = NotificationCompat.Action.Builder(R.drawable.ic_delete, dismissLabel, dismissPendingIntent) + .setShowsUserInterface(false) + .setAllowGeneratedReplies(true) + .build() + notificationBuilder.addAction(dismissAction) + } + + private fun addShareRecordingToChatAction( + notificationBuilder: NotificationCompat.Builder, + systemNotificationId: Int, + ncNotification: com.nextcloud.talk.models.json.notifications.Notification + ) { + var shareToChatLabel = "" + var shareToChatUrl = "" + + for (action in ncNotification.actions!!) { + if (action.primary) { + shareToChatLabel = action.label.orEmpty() + shareToChatUrl = action.link.orEmpty() + } + } + + val shareRecordingIntent = Intent(context, ShareRecordingToChatReceiver::class.java) + shareRecordingIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId) + shareRecordingIntent.putExtra(KEY_SHARE_RECORDING_TO_CHAT_URL, shareToChatUrl) + shareRecordingIntent.putExtra(KEY_ROOM_TOKEN, pushMessage.id) + + val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + val shareRecordingPendingIntent = PendingIntent.getBroadcast( + context, + systemNotificationId, + shareRecordingIntent, + intentFlag + ) + + val shareRecordingAction = NotificationCompat.Action.Builder( + R.drawable.ic_delete, + shareToChatLabel, + shareRecordingPendingIntent + ) + .setShowsUserInterface(false) + .setAllowGeneratedReplies(true) + .build() + notificationBuilder.addAction(shareRecordingAction) + } + + private fun getStyle(person: Person, style: NotificationCompat.MessagingStyle?): NotificationCompat.MessagingStyle { + val newStyle = NotificationCompat.MessagingStyle(person) + newStyle.conversationTitle = pushMessage.subject + newStyle.isGroupConversation = "one2one" != conversationType + style?.messages?.forEach( + Consumer { message: NotificationCompat.MessagingStyle.Message -> + newStyle.addMessage( + NotificationCompat.MessagingStyle.Message( + message.text, + message.timestamp, + message.person + ) + ) + } + ) + newStyle.addMessage(pushMessage.text, pushMessage.timestamp, person) + return newStyle + } + + @Throws(NumberFormatException::class) + private fun parseMessageId(objectId: String): Int { + val objectIdParts = objectId.split("/".toRegex()).toTypedArray() + return if (objectIdParts.size < 2) { + throw NumberFormatException("Invalid objectId, doesn't contain at least one '/'") + } else { + objectIdParts[1].toInt() + } + } + + private fun sendNotification(notificationId: Int, notification: Notification) { + Log.d(TAG, "show notification with id $notificationId") + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return + } + notificationManager.notify(notificationId, notification) + + return + } + + private fun removeNotification(notificationId: Int) { + Log.d(TAG, "removed notification with id $notificationId") + notificationManager.cancel(notificationId) + } + + private fun checkIfCallIsActive(signatureVerification: SignatureVerification, conversation: ConversationModel) { + Log.d(TAG, "checkIfCallIsActive") + var hasParticipantsInCall = true + var inCallOnDifferentDevice = false + + val apiVersion = ApiUtils.getConversationApiVersion( + signatureVerification.user!!, + intArrayOf(ApiUtils.API_V4, 1) + ) + + var isCallNotificationVisible = true + + ncApi.getPeersForCall( + credentials, + ApiUtils.getUrlForCall( + apiVersion, + signatureVerification.user!!.baseUrl!!, + pushMessage.id!! + ) + ) + .repeatWhen { completed -> + completed.zipWith(Observable.range(TIMER_START, TIMER_COUNT)) { _, i -> i } + .flatMap { Observable.timer(TIMER_DELAY, TimeUnit.SECONDS) } + .takeWhile { isCallNotificationVisible && hasParticipantsInCall && !inCallOnDifferentDevice } + } + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(participantsOverall: ParticipantsOverall) { + val participantList: List = participantsOverall.ocs!!.data!! + hasParticipantsInCall = participantList.isNotEmpty() + if (hasParticipantsInCall) { + for (participant in participantList) { + if (participant.actorId == signatureVerification.user!!.userId && + participant.actorType == Participant.ActorType.USERS + ) { + inCallOnDifferentDevice = true + break + } + } + } + if (inCallOnDifferentDevice) { + Log.d(TAG, "inCallOnDifferentDevice is true") + removeNotification(pushMessage.timestamp.toInt()) + } + + if (!hasParticipantsInCall) { + showMissedCallNotification(conversation) + Log.d(TAG, "no participants in call") + removeNotification(pushMessage.timestamp.toInt()) + } + + isCallNotificationVisible = NotificationUtils.isNotificationVisible( + context, + pushMessage.timestamp.toInt() + ) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error in getPeersForCall", e) + if (isCallNotificationVisible) { + showMissedCallNotification(conversation) + } + removeNotification(pushMessage.timestamp.toInt()) + } + + override fun onComplete() { + if (isCallNotificationVisible) { + // this state can be reached when call timeout is reached. + showMissedCallNotification(conversation) + } + + removeNotification(pushMessage.timestamp.toInt()) + } + }) + } + + fun showMissedCallNotification(conversation: ConversationModel) { + val isOngoingCallNotificationVisible = NotificationUtils.isNotificationVisible( + context, + pushMessage.timestamp.toInt() + ) + + if (isOngoingCallNotificationVisible) { + val notificationBuilder = NotificationCompat.Builder( + context!!, + NotificationUtils.NotificationChannels + .NOTIFICATION_CHANNEL_MESSAGES_V4.name + ) + + val intent = createMainActivityIntent() + + val notification: Notification = notificationBuilder + .setContentTitle( + String.format( + context!!.resources.getString(R.string.nc_missed_call), + conversation.displayName + ) + ) + .setSmallIcon(R.drawable.ic_baseline_phone_missed_24) + .setOngoing(false) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setContentIntent(createUniquePendingIntent(intent)) + .build() + + val notificationId: Int = SystemClock.uptimeMillis().toInt() + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return + } + notificationManager.notify(notificationId, notification) + Log.d(TAG, "'you missed a call' notification was created") + } + } + + private fun createMainActivityIntent(): Intent { + val intent = Intent(context, MainActivity::class.java) + intent.flags = getIntentFlags() + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, pushMessage.id) + bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!) + intent.putExtras(bundle) + return intent + } + + private fun createUniquePendingIntent(intent: Intent): PendingIntent? { + // Use unique request code to make sure that a new PendingIntent gets created for each notification + // See https://github.com/nextcloud/talk-android/issues/2111 + val requestCode = System.currentTimeMillis().toInt() + val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + return PendingIntent.getActivity(context, requestCode, intent, intentFlag) + } + + private fun getIntentFlags(): Int = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + + companion object { + val TAG = NotificationWorker::class.simpleName + private const val TYPE_CHAT = "chat" + private const val TYPE_ROOM = "room" + private const val TYPE_CALL = "call" + private const val TYPE_RECORDING = "recording" + private const val TYPE_REMOTE_TALK_SHARE = "remote_talk_share" + private const val TYPE_REMINDER = "reminder" + private const val TYPE_ADMIN_NOTIFICATIONS = "admin_notifications" + private const val SPREED_APP = "spreed" + private const val ADMIN_NOTIFICATION_TALK = "admin_notification_talk" + private const val TIMER_START = 1 + private const val TIMER_COUNT = 12 + private const val TIMER_DELAY: Long = 5 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java new file mode 100644 index 0000000..6473d0e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java @@ -0,0 +1,75 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import autodagger.AutoInjector; +import okhttp3.JavaNetCookieJar; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; + +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.utils.ClosedInterfaceImpl; +import com.nextcloud.talk.utils.PushUtils; + +import java.net.CookieManager; + +import javax.inject.Inject; + +@AutoInjector(NextcloudTalkApplication.class) +public class PushRegistrationWorker extends Worker { + public static final String TAG = "PushRegistrationWorker"; + public static final String ORIGIN = "origin"; + + @Inject + Retrofit retrofit; + + @Inject + OkHttpClient okHttpClient; + + public PushRegistrationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + if (new ClosedInterfaceImpl().isGooglePlayServicesAvailable()) { + Data data = getInputData(); + String origin = data.getString("origin"); + Log.d(TAG, "PushRegistrationWorker called via " + origin); + + NcApi ncApi = retrofit + .newBuilder() + .client(okHttpClient + .newBuilder() + .cookieJar(new JavaNetCookieJar(new CookieManager())) + .build()) + .build() + .create(NcApi.class); + + PushUtils pushUtils = new PushUtils(); + pushUtils.generateRsa2048KeyPair(); + pushUtils.pushRegistrationToServer(ncApi); + + return Result.success(); + } + Log.w(TAG, "executing PushRegistrationWorker doesn't make sense because Google Play Services are not " + + "available"); + return Result.failure(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt new file mode 100644 index 0000000..a84aade --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/SaveFileToStorageWorker.kt @@ -0,0 +1,115 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Fariba Khandani + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs + +import android.content.ContentValues +import android.content.Context +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.os.Handler +import android.os.Looper +import android.provider.MediaStore +import android.provider.MediaStore.Files.FileColumns +import android.util.Log +import android.widget.Toast +import androidx.work.Worker +import androidx.work.WorkerParameters +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.utils.Mimetype.AUDIO_PREFIX +import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX +import com.nextcloud.talk.utils.Mimetype.VIDEO_PREFIX +import java.io.File +import java.io.IOException +import java.io.OutputStream +import java.net.URLConnection + +@AutoInjector(NextcloudTalkApplication::class) +class SaveFileToStorageWorker(val context: Context, workerParameters: WorkerParameters) : + Worker(context, workerParameters) { + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun doWork(): Result { + try { + val cacheFile = File(inputData.getString(KEY_SOURCE_FILE_PATH)!!) + val contentResolver = context.contentResolver + val mimeType = URLConnection.guessContentTypeFromName(cacheFile.name) + + val values = ContentValues().apply { + if (mimeType.startsWith(IMAGE_PREFIX) || mimeType.startsWith(VIDEO_PREFIX)) { + val appName = applicationContext.resources!!.getString(R.string.nc_app_product_name) + put(FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/" + appName) + } + put(FileColumns.DISPLAY_NAME, cacheFile.name) + + if (mimeType != null) { + put(FileColumns.MIME_TYPE, mimeType) + } + } + + contentResolver.insert(getUriByType(mimeType), values)?.let { fileUri -> + try { + val outputStream: OutputStream? = contentResolver.openOutputStream(fileUri) + outputStream.use { output -> + val inputStream = cacheFile.inputStream() + if (output != null) { + inputStream.copyTo(output) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to create output stream") + return Result.failure() + } + } + + // Notify the media scanner about the new file + MediaScannerConnection.scanFile(context, arrayOf(cacheFile.absolutePath), null, null) + + Handler(Looper.getMainLooper()).post { + Toast.makeText(context, R.string.nc_save_success, Toast.LENGTH_SHORT).show() + } + + return Result.success() + } catch (e: IOException) { + Log.e(TAG, "Something went wrong when trying to save file to internal storage", e) + Handler(Looper.getMainLooper()).post { + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_SHORT).show() + } + + return Result.failure() + } catch (e: NullPointerException) { + Log.e(TAG, "Something went wrong when trying to save file to internal storage", e) + Handler(Looper.getMainLooper()).post { + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_SHORT).show() + } + + return Result.failure() + } + } + + private fun getUriByType(mimeType: String): Uri = + when { + mimeType.startsWith(VIDEO_PREFIX) -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + mimeType.startsWith(AUDIO_PREFIX) -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + mimeType.startsWith(IMAGE_PREFIX) -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + else -> if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + Uri.fromFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)) + } else { + MediaStore.Downloads.EXTERNAL_CONTENT_URI + } + } + + companion object { + private val TAG = SaveFileToStorageWorker::class.java.simpleName + const val KEY_FILE_NAME = "KEY_FILE_NAME" + const val KEY_SOURCE_FILE_PATH = "KEY_SOURCE_FILE_PATH" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt new file mode 100644 index 0000000..0aed903 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt @@ -0,0 +1,98 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs + +import android.content.Context +import android.util.Log +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import autodagger.AutoInjector +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_META_DATA +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ShareOperationWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var ncApi: NcApi + + private val userId: Long + private val roomToken: String? + private val filesArray: MutableList = ArrayList() + private val credentials: String + private val baseUrl: String? + private val metaData: String? + + override fun doWork(): Result { + for (filePath in filesArray) { + ncApi.createRemoteShare( + credentials, + ApiUtils.getSharingUrl(baseUrl!!), + filePath, + roomToken, + "10", + metaData + ) + .subscribeOn(Schedulers.io()) + .blockingSubscribe( + {}, + { e -> Log.w(TAG, "error while creating RemoteShare", e) } + ) + } + return Result.success() + } + + init { + sharedApplication!!.componentApplication.inject(this) + val data = workerParams.inputData + userId = data.getLong(KEY_INTERNAL_USER_ID, 0) + roomToken = data.getString(KEY_ROOM_TOKEN) + metaData = data.getString(KEY_META_DATA) + data.getStringArray(KEY_FILE_PATHS)?.let { filesArray.addAll(it.toList()) } + + val operationsUser = userManager.getUserWithId(userId).blockingGet() + baseUrl = operationsUser.baseUrl + credentials = ApiUtils.getCredentials(operationsUser.username, operationsUser.token)!! + } + + companion object { + private val TAG = ShareOperationWorker::class.simpleName + + fun shareFile(roomToken: String?, currentUser: User, remotePath: String, metaData: String?) { + val paths: MutableList = ArrayList() + paths.add(remotePath) + + val data = Data.Builder() + .putLong(KEY_INTERNAL_USER_ID, currentUser.id!!) + .putString(KEY_ROOM_TOKEN, roomToken) + .putStringArray(KEY_FILE_PATHS, paths.toTypedArray()) + .putString(KEY_META_DATA, metaData) + .build() + val shareWorker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java) + .setInputData(data) + .build() + WorkManager.getInstance().enqueue(shareWorker) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java new file mode 100644 index 0000000..0e59641 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java @@ -0,0 +1,149 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs; + +import android.content.Context; + +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.events.EventStatus; +import com.nextcloud.talk.models.ExternalSignalingServer; +import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall; +import com.nextcloud.talk.users.UserManager; +import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.UserIdUtils; +import com.nextcloud.talk.utils.bundle.BundleKeys; + +import org.greenrobot.eventbus.EventBus; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import autodagger.AutoInjector; +import io.reactivex.Observer; +import io.reactivex.SingleObserver; +import io.reactivex.disposables.Disposable; + +@AutoInjector(NextcloudTalkApplication.class) +public class SignalingSettingsWorker extends Worker { + + @Inject + UserManager userManager; + + @Inject + NcApi ncApi; + + @Inject + EventBus eventBus; + + public SignalingSettingsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + + Data data = getInputData(); + + long internalUserId = data.getLong(BundleKeys.KEY_INTERNAL_USER_ID, -1); + + List userEntityObjectList = new ArrayList<>(); + boolean userNotFound = userManager.getUserWithInternalId(internalUserId).isEmpty().blockingGet(); + + if (internalUserId == -1 || userNotFound) { + userEntityObjectList = userManager.getUsers().blockingGet(); + } else { + userEntityObjectList.add(userManager.getUserWithInternalId(internalUserId).blockingGet()); + } + + for (User user : userEntityObjectList) { + + int apiVersion = ApiUtils.getSignalingApiVersion(user, new int[] {ApiUtils.API_V3, 2, 1}); + + ncApi.getSignalingSettings( + ApiUtils.getCredentials(user.getUsername(), user.getToken()), + ApiUtils.getUrlForSignalingSettings(apiVersion, user.getBaseUrl())) + .blockingSubscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { + // unused stm + } + + @Override + public void onNext(SignalingSettingsOverall signalingSettingsOverall) { + ExternalSignalingServer externalSignalingServer; + externalSignalingServer = new ExternalSignalingServer(); + + if (signalingSettingsOverall.getOcs() != null && + signalingSettingsOverall.getOcs().getSettings() != null) { + externalSignalingServer.setExternalSignalingServer(signalingSettingsOverall + .getOcs() + .getSettings() + .getExternalSignalingServer()); + externalSignalingServer.setExternalSignalingTicket(signalingSettingsOverall + .getOcs() + .getSettings() + .getExternalSignalingTicket()); + } + + user.setExternalSignalingServer(externalSignalingServer); + + userManager.saveUser(user).subscribe(new SingleObserver() { + @Override + public void onSubscribe(Disposable d) { + // unused atm + } + + @Override + public void onSuccess(Integer rows) { + if (rows > 0) { + eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user), + EventStatus.EventType.SIGNALING_SETTINGS, + true)); + } else { + eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user), + EventStatus.EventType.SIGNALING_SETTINGS, + false)); + } + } + + @Override + public void onError(Throwable e) { + eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user), + EventStatus.EventType.SIGNALING_SETTINGS, + false)); + } + }); + } + + @Override + public void onError(Throwable e) { + eventBus.post(new EventStatus(UserIdUtils.INSTANCE.getIdForUser(user), + EventStatus.EventType.SIGNALING_SETTINGS, + false)); + } + + @Override + public void onComplete() { + // unused atm + } + }); + } + + return Result.success(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt new file mode 100644 index 0000000..0ff9096 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt @@ -0,0 +1,365 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Parneet Singh + * SPDX-FileCopyrightText: 2021-2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs + +import android.Manifest +import android.app.Activity +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.SystemClock +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.net.toUri +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.MainActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.upload.chunked.ChunkedFileUploader +import com.nextcloud.talk.upload.chunked.OnDataTransferProgressListener +import com.nextcloud.talk.upload.normal.FileUploader +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.FileUtils +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.RemoteFileUtils +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil +import com.nextcloud.talk.utils.preferences.AppPreferences +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import java.io.File +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerParameters) : + Worker(context, workerParameters), + OnDataTransferProgressListener { + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var okHttpClient: OkHttpClient + + @Inject + lateinit var platformPermissionUtil: PlatformPermissionUtil + + lateinit var fileName: String + + private var mNotifyManager: NotificationManager? = null + private var mBuilder: NotificationCompat.Builder? = null + private var notificationId: Int = 0 + + lateinit var roomToken: String + lateinit var conversationName: String + lateinit var currentUser: User + private var isChunkedUploading = false + private var file: File? = null + private var chunkedFileUploader: ChunkedFileUploader? = null + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun doWork(): Result { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + return try { + currentUser = currentUserProvider.currentUser.blockingGet() + val sourceFile = inputData.getString(DEVICE_SOURCE_FILE) + roomToken = inputData.getString(ROOM_TOKEN)!! + conversationName = inputData.getString(CONVERSATION_NAME)!! + val metaData = inputData.getString(META_DATA) + + checkNotNull(currentUser) + checkNotNull(sourceFile) + require(sourceFile.isNotEmpty()) + checkNotNull(roomToken) + + val sourceFileUri = sourceFile.toUri() + fileName = FileUtils.getFileName(sourceFileUri, context) + file = FileUtils.getFileFromUri(context, sourceFileUri) + val remotePath = getRemotePath(currentUser) + + initNotificationSetup() + file?.let { isChunkedUploading = it.length() > CHUNK_UPLOAD_THRESHOLD_SIZE } + val uploadSuccess: Boolean = uploadFile(sourceFileUri, metaData, remotePath) + + if (uploadSuccess) { + cancelNotification() + return Result.success() + } else if (isStopped) { + // since work is cancelled the result would be ignored anyways + return Result.failure() + } + + Log.e(TAG, "Something went wrong when trying to upload file") + showFailedToUploadNotification() + return Result.failure() + } catch (e: Exception) { + Log.e(TAG, "Something went wrong when trying to upload file", e) + showFailedToUploadNotification() + return Result.failure() + } + } + + private fun uploadFile(sourceFileUri: Uri, metaData: String?, remotePath: String): Boolean = + if (file == null) { + false + } else if (isChunkedUploading) { + Log.d(TAG, "starting chunked upload because size is " + file!!.length()) + + initNotificationWithPercentage() + val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull() + + chunkedFileUploader = ChunkedFileUploader(okHttpClient, currentUser, roomToken, metaData, this) + chunkedFileUploader!!.upload(file!!, mimeType, remotePath) + } else { + Log.d(TAG, "starting normal upload (not chunked) of $fileName") + + FileUploader(okHttpClient, context, currentUser, roomToken, ncApi, file!!) + .upload(sourceFileUri, fileName, remotePath, metaData) + .blockingFirst() + } + + private fun getRemotePath(currentUser: User): String { + val remotePath = CapabilitiesUtil.getAttachmentFolder( + currentUser.capabilities!!.spreedCapability!! + ) + "/" + fileName + return RemoteFileUtils.getNewPathIfFileExists(ncApi, currentUser, remotePath) + } + + override fun onTransferProgress(percentage: Int) { + val progressUpdateNotification = mBuilder!! + .setProgress(HUNDRED_PERCENT, percentage, false) + .setContentText(getNotificationContentText(percentage)) + .build() + + mNotifyManager!!.notify(notificationId, progressUpdateNotification) + } + + override fun onStopped() { + if (file != null && isChunkedUploading) { + chunkedFileUploader?.abortUpload { + mNotifyManager?.cancel(notificationId) + } + } + super.onStopped() + } + + private fun initNotificationSetup() { + mNotifyManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + mBuilder = NotificationCompat.Builder( + context, + NotificationUtils.NotificationChannels + .NOTIFICATION_CHANNEL_UPLOADS.name + ) + } + + private fun initNotificationWithPercentage() { + val initNotification = mBuilder!! + .setContentTitle(context.resources.getString(R.string.nc_upload_in_progess)) + .setContentText(getNotificationContentText(ZERO_PERCENT)) + .setSmallIcon(R.drawable.upload_white) + .setOngoing(true) + .setProgress(HUNDRED_PERCENT, ZERO_PERCENT, false) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setGroup(NotificationUtils.KEY_UPLOAD_GROUP) + .setContentIntent(getIntentToOpenConversation()) + .addAction( + R.drawable.ic_cancel_white_24dp, + getResourceString(context, R.string.nc_cancel), + getCancelUploadIntent() + ) + .build() + + notificationId = SystemClock.uptimeMillis().toInt() + mNotifyManager!!.notify(notificationId, initNotification) + // only need one summary notification but multiple upload worker can call it more than once but it is safe + // because of the same notification object config and id. + makeSummaryNotification() + } + + private fun makeSummaryNotification() { + // summary notification encapsulating the group of notifications + val summaryNotification = NotificationCompat.Builder( + context, + NotificationUtils.NotificationChannels + .NOTIFICATION_CHANNEL_UPLOADS.name + ).setSmallIcon(R.drawable.upload_white) + .setGroup(NotificationUtils.KEY_UPLOAD_GROUP) + .setGroupSummary(true) + .build() + + mNotifyManager?.notify(NotificationUtils.GROUP_SUMMARY_NOTIFICATION_ID, summaryNotification) + } + + private fun getActiveUploadNotifications(): Int? { + // filter out active notifications that are upload notifications using group + return mNotifyManager?.activeNotifications?.filter { + it.notification.group == NotificationUtils + .KEY_UPLOAD_GROUP + }?.size + } + + private fun cancelNotification() { + mNotifyManager?.cancel(notificationId) + // summary notification would not get dismissed automatically + // if child notifications are cancelled programmatically + // so check if only 1 notification left if yes + // then cancel it (which is summary notification) + if (getActiveUploadNotifications() == 1) { + mNotifyManager?.cancel(NotificationUtils.GROUP_SUMMARY_NOTIFICATION_ID) + } + } + + private fun getNotificationContentText(percentage: Int): String = + String.format( + getResourceString(context, R.string.nc_upload_notification_text), + getShortenedFileName(), + conversationName, + percentage + ) + + private fun getShortenedFileName(): String = + if (fileName.length > NOTIFICATION_FILE_NAME_MAX_LENGTH) { + THREE_DOTS + fileName.takeLast(NOTIFICATION_FILE_NAME_MAX_LENGTH) + } else { + fileName + } + + private fun getCancelUploadIntent(): PendingIntent = + WorkManager.getInstance(applicationContext) + .createCancelPendingIntent(id) + + private fun getIntentToOpenConversation(): PendingIntent? { + val bundle = Bundle() + val intent = Intent(context, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + + bundle.putString(KEY_ROOM_TOKEN, roomToken) + bundle.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!) + + intent.putExtras(bundle) + + val requestCode = System.currentTimeMillis().toInt() + val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE + } else { + 0 + } + return PendingIntent.getActivity(context, requestCode, intent, intentFlag) + } + + private fun showFailedToUploadNotification() { + val failureTitle = getResourceString(context, R.string.nc_upload_failed_notification_title) + val failureText = String.format( + getResourceString(context, R.string.nc_upload_failed_notification_text), + fileName + ) + val failureNotification = NotificationCompat.Builder( + context, + NotificationUtils.NotificationChannels + .NOTIFICATION_CHANNEL_UPLOADS.name + ) + .setContentTitle(failureTitle) + .setContentText(failureText) + .setSmallIcon(R.drawable.baseline_error_24) + .setGroup(NotificationUtils.KEY_UPLOAD_GROUP) + .setOngoing(false) + .build() + + mNotifyManager?.cancel(notificationId) + // update current notification with failure info + mNotifyManager!!.notify(SystemClock.uptimeMillis().toInt(), failureNotification) + } + + private fun getResourceString(context: Context, resourceId: Int): String = context.resources.getString(resourceId) + + companion object { + private val TAG = UploadAndShareFilesWorker::class.simpleName + private const val DEVICE_SOURCE_FILE = "DEVICE_SOURCE_FILE" + private const val ROOM_TOKEN = "ROOM_TOKEN" + private const val CONVERSATION_NAME = "CONVERSATION_NAME" + private const val META_DATA = "META_DATA" + private const val CHUNK_UPLOAD_THRESHOLD_SIZE: Long = 1024000 + private const val NOTIFICATION_FILE_NAME_MAX_LENGTH = 20 + private const val THREE_DOTS = "…" + private const val HUNDRED_PERCENT = 100 + private const val ZERO_PERCENT = 0 + const val REQUEST_PERMISSION = 3123 + + fun requestStoragePermission(activity: Activity) { + when { + Build.VERSION + .SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { + activity.requestPermissions( + arrayOf( + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.READ_MEDIA_AUDIO + ), + REQUEST_PERMISSION + ) + } + + Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> { + activity.requestPermissions( + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE + ), + REQUEST_PERMISSION + ) + } + + else -> { + activity.requestPermissions( + arrayOf( + Manifest.permission.WRITE_EXTERNAL_STORAGE + ), + REQUEST_PERMISSION + ) + } + } + } + + fun upload(fileUri: String, roomToken: String, conversationName: String, metaData: String?) { + val data: Data = Data.Builder() + .putString(DEVICE_SOURCE_FILE, fileUri) + .putString(ROOM_TOKEN, roomToken) + .putString(CONVERSATION_NAME, conversationName) + .putString(META_DATA, metaData) + .build() + val uploadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java) + .setInputData(data) + .build() + WorkManager.getInstance().enqueueUniqueWork(fileUri, ExistingWorkPolicy.KEEP, uploadWorker) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.java new file mode 100644 index 0000000..e0581d9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.java @@ -0,0 +1,70 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.jobs; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.users.UserManager; +import com.nextcloud.talk.webrtc.WebSocketConnectionHelper; + +import java.util.List; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import autodagger.AutoInjector; + +@AutoInjector(NextcloudTalkApplication.class) +public class WebsocketConnectionsWorker extends Worker { + + public static final String TAG = "WebsocketConnectionsWorker"; + + @Inject + UserManager userManager; + + public WebsocketConnectionsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @SuppressLint("LongLogTag") + @NonNull + @Override + public Result doWork() { + Log.d(TAG, "WebsocketConnectionsWorker started "); + + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + + List users = userManager.getUsers().blockingGet(); + for (User user : users) { + if (user.getExternalSignalingServer() != null && + user.getExternalSignalingServer().getExternalSignalingServer() != null && + !TextUtils.isEmpty(user.getExternalSignalingServer().getExternalSignalingServer()) && + !TextUtils.isEmpty(user.getExternalSignalingServer().getExternalSignalingTicket())) { + + Log.d(TAG, "trying to getExternalSignalingInstanceForServer for user " + user.getDisplayName()); + + WebSocketConnectionHelper.getExternalSignalingInstanceForServer( + user.getExternalSignalingServer().getExternalSignalingServer(), + user, + user.getExternalSignalingServer().getExternalSignalingTicket(), + false); + } else { + Log.d(TAG, "skipped to getExternalSignalingInstanceForServer for user " + user.getDisplayName()); + } + } + + return Result.success(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt new file mode 100644 index 0000000..2e774ea --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/location/GeocodingActivity.kt @@ -0,0 +1,223 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.location + +import android.app.SearchManager +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.text.InputType +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.inputmethod.EditorInfo +import androidx.appcompat.widget.SearchView +import androidx.core.graphics.drawable.toDrawable +import androidx.lifecycle.ViewModelProvider +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.adapters.GeocodingAdapter +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.ActivityGeocodingBinding +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.viewmodels.GeoCodingViewModel +import fr.dudie.nominatim.client.TalkJsonNominatimClient +import fr.dudie.nominatim.model.Address +import okhttp3.OkHttpClient +import org.osmdroid.config.Configuration +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class GeocodingActivity : BaseActivity() { + + private lateinit var binding: ActivityGeocodingBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var okHttpClient: OkHttpClient + + lateinit var roomToken: String + private var chatApiVersion: Int = 1 + private var nominatimClient: TalkJsonNominatimClient? = null + + private var searchItem: MenuItem? = null + var searchView: SearchView? = null + + lateinit var adapter: GeocodingAdapter + private var geocodingResults: List

= ArrayList() + private lateinit var recyclerView: RecyclerView + private lateinit var viewModel: GeoCodingViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityGeocodingBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + initSystemBars() + + Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context)) + + roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!! + chatApiVersion = intent.getIntExtra(BundleKeys.KEY_CHAT_API_VERSION, 1) + + recyclerView = findViewById(R.id.geocoding_results) + recyclerView.layoutManager = LinearLayoutManager(this) + adapter = GeocodingAdapter(this, geocodingResults) + recyclerView.adapter = adapter + viewModel = ViewModelProvider(this)[GeoCodingViewModel::class.java] + + var query = viewModel.getQuery() + if (query.isEmpty() && intent.hasExtra(BundleKeys.KEY_GEOCODING_QUERY)) { + query = intent.getStringExtra(BundleKeys.KEY_GEOCODING_QUERY).orEmpty() + viewModel.setQuery(query) + } + val savedResults = viewModel.getGeocodingResults() + initAdapter(savedResults) + viewModel.getGeocodingResultsLiveData().observe(this) { results -> + geocodingResults = results + adapter.updateData(results) + } + val baseUrl = getString(R.string.osm_geocoder_url) + val email = context.getString(R.string.osm_geocoder_contact) + nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email) + } + + override fun onStart() { + super.onStart() + initAdapter(geocodingResults) + initGeocoder() + } + + override fun onResume() { + super.onResume() + + if (viewModel.getQuery().isNotEmpty() && adapter.itemCount == 0) { + viewModel.searchLocation() + } else { + Log.e(TAG, "search string that was passed to GeocodingActivity was null or empty") + } + adapter.setOnItemClickListener(object : GeocodingAdapter.OnItemClickListener { + override fun onItemClick(position: Int) { + val address: Address = adapter.getItem(position) as Address + val geocodingResult = GeocodingResult(address.latitude, address.longitude, address.displayName) + val intent = Intent(this@GeocodingActivity, LocationPickerActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken) + intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion) + intent.putExtra(BundleKeys.KEY_GEOCODING_RESULT, geocodingResult) + startActivity(intent) + } + }) + searchView?.setQuery(viewModel.getQuery(), false) + } + + private fun setupActionBar() { + setSupportActionBar(binding.geocodingToolbar) + binding.geocodingToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(R.color.transparent, null).toDrawable()) + supportActionBar?.title = "" + viewThemeUtils.material.themeToolbar(binding.geocodingToolbar) + } + + private fun initAdapter(addresses: List
) { + adapter = GeocodingAdapter(binding.geocodingResults.context!!, addresses) + adapter.setOnItemClickListener(object : GeocodingAdapter.OnItemClickListener { + override fun onItemClick(position: Int) { + val address: Address = adapter.getItem(position) as Address + val geocodingResult = GeocodingResult(address.latitude, address.longitude, address.displayName) + val intent = Intent(this@GeocodingActivity, LocationPickerActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken) + intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion) + intent.putExtra(BundleKeys.KEY_GEOCODING_RESULT, geocodingResult) + startActivity(intent) + } + }) + binding.geocodingResults.adapter = adapter + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.menu_geocoding, menu) + searchItem = menu.findItem(R.id.geocoding_action_search) + initSearchView() + searchItem?.expandActionView() + searchView?.setQuery(viewModel.getQuery(), false) + searchView?.clearFocus() + return true + } + + private fun initSearchView() { + val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager + if (searchItem != null) { + searchView = searchItem!!.actionView as SearchView? + + searchView?.maxWidth = Int.MAX_VALUE + searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER + var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN + if (appPreferences.isKeyboardIncognito) { + imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } + searchView?.imeOptions = imeOptions + searchView?.queryHint = resources!!.getString(R.string.nc_search) + searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName)) + searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + viewModel.setQuery(query) + viewModel.searchLocation() + searchView?.clearFocus() + return true + } + + override fun onQueryTextChange(query: String): Boolean { + // This is a workaround to not set viewModel data when onQueryTextChange is triggered on startup + // Otherwise it would be set to an empty string. + if (searchView?.width!! > 0) { + viewModel.setQuery(query) + } + return true + } + }) + + searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean = true + + override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean { + val intent = Intent(context, LocationPickerActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, roomToken) + intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion) + startActivity(intent) + return true + } + }) + } + } + + private fun initGeocoder() { + val baseUrl = getString(R.string.osm_geocoder_url) + val email = context.getString(R.string.osm_geocoder_contact) + nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email) + } + + companion object { + val TAG = GeocodingActivity::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/location/GeocodingResult.kt b/app/src/main/java/com/nextcloud/talk/location/GeocodingResult.kt new file mode 100644 index 0000000..9d07eb9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/location/GeocodingResult.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.location + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class GeocodingResult(val lat: Double, val lon: Double, var displayName: String) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt new file mode 100644 index 0000000..03c356f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/location/LocationPickerActivity.kt @@ -0,0 +1,589 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.location + +import android.Manifest +import android.app.Activity +import android.app.SearchManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.text.InputType +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.widget.SearchView +import androidx.core.content.PermissionChecker +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.toDrawable +import androidx.core.view.MenuItemCompat +import androidx.preference.PreferenceManager +import autodagger.AutoInjector +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.ActivityLocationBinding +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CHAT_API_VERSION +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_GEOCODING_RESULT +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import fr.dudie.nominatim.client.TalkJsonNominatimClient +import fr.dudie.nominatim.model.Address +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import org.osmdroid.config.Configuration.getInstance +import org.osmdroid.events.DelayedMapListener +import org.osmdroid.events.MapListener +import org.osmdroid.events.ScrollEvent +import org.osmdroid.events.ZoomEvent +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.overlay.CopyrightOverlay +import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider +import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class LocationPickerActivity : + BaseActivity(), + SearchView.OnQueryTextListener, + LocationListener { + + private lateinit var binding: ActivityLocationBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var okHttpClient: OkHttpClient + + var nominatimClient: TalkJsonNominatimClient? = null + + lateinit var roomToken: String + private var chatApiVersion: Int = 1 + var geocodingResult: GeocodingResult? = null + + var myLocation: GeoPoint = GeoPoint(COORDINATE_ZERO, COORDINATE_ZERO) + private var locationManager: LocationManager? = null + private lateinit var locationOverlay: MyLocationNewOverlay + + var moveToCurrentLocation: Boolean = true + var readyToShareLocation: Boolean = false + + private var mapCenterLat: Double = 0.0 + private var mapCenterLon: Double = 0.0 + + var searchItem: MenuItem? = null + var searchView: SearchView? = null + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + setResult(Activity.RESULT_CANCELED) + finish() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)!! + chatApiVersion = intent.getIntExtra(KEY_CHAT_API_VERSION, 1) + geocodingResult = intent.getParcelableExtra(KEY_GEOCODING_RESULT) + + if (savedInstanceState != null) { + moveToCurrentLocation = savedInstanceState.getBoolean("moveToCurrentLocation") == true + mapCenterLat = savedInstanceState.getDouble("mapCenterLat") + mapCenterLon = savedInstanceState.getDouble("mapCenterLon") + geocodingResult = savedInstanceState.getParcelable("geocodingResult") + } + + binding = ActivityLocationBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + initSystemBars() + + getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context)) + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + override fun onStart() { + super.onStart() + initMap() + } + + override fun onResume() { + super.onResume() + + if (geocodingResult != null) { + moveToCurrentLocation = false + } + + setLocationDescription(false, geocodingResult != null) + binding.shareLocation.isClickable = false + binding.shareLocation.setOnClickListener { + if (readyToShareLocation) { + shareLocation( + binding.map.mapCenter?.latitude, + binding.map.mapCenter?.longitude, + binding.placeName.text.toString() + ) + } else { + Log.w(TAG, "readyToShareLocation was false while user tried to share location.") + } + } + } + + override fun onSaveInstanceState(bundle: Bundle) { + super.onSaveInstanceState(bundle) + bundle.putBoolean("moveToCurrentLocation", moveToCurrentLocation) + bundle.putDouble("mapCenterLat", binding.map.mapCenter.latitude) + bundle.putDouble("mapCenterLon", binding.map.mapCenter.longitude) + bundle.putParcelable("geocodingResult", geocodingResult) + } + + private fun setupActionBar() { + setSupportActionBar(binding.locationPickerToolbar) + binding.locationPickerToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(android.R.color.transparent, null).toDrawable()) + supportActionBar?.title = context.getString(R.string.nc_share_location) + viewThemeUtils.material.themeToolbar(binding.locationPickerToolbar) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.menu_locationpicker, menu) + return true + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + super.onPrepareOptionsMenu(menu) + searchItem = menu.findItem(R.id.location_action_search) + initSearchView() + return true + } + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onStop() { + super.onStop() + + try { + locationManager!!.removeUpdates(this) + } catch (e: Exception) { + Log.e(TAG, "error when trying to remove updates for location Manager", e) + } + + locationOverlay.disableMyLocation() + } + + private fun initSearchView() { + val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager + if (searchItem != null) { + searchView = MenuItemCompat.getActionView(searchItem) as SearchView + searchView?.maxWidth = Int.MAX_VALUE + searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER + var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN + if (appPreferences!!.isKeyboardIncognito) { + imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } + searchView?.imeOptions = imeOptions + searchView?.queryHint = resources!!.getString(R.string.nc_search) + searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName)) + searchView?.setOnQueryTextListener(this) + } + } + + override fun onQueryTextSubmit(query: String?): Boolean { + if (!query.isNullOrEmpty()) { + val intent = Intent(this, GeocodingActivity::class.java) + intent.putExtra(BundleKeys.KEY_GEOCODING_QUERY, query) + intent.putExtra(KEY_ROOM_TOKEN, roomToken) + intent.putExtra(KEY_CHAT_API_VERSION, chatApiVersion) + startActivity(intent) + } + return true + } + + override fun onQueryTextChange(newText: String?): Boolean = true + + @Suppress("Detekt.TooGenericExceptionCaught", "Detekt.ComplexMethod", "Detekt.LongMethod") + private fun initMap() { + binding.map.setTileSource(TileSourceFactory.MAPNIK) + binding.map.onResume() + + locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager + + if (!isLocationPermissionsGranted()) { + requestLocationPermissions() + } else { + requestLocationUpdates() + } + + val copyrightOverlay = CopyrightOverlay(context) + binding.map.overlays.add(copyrightOverlay) + + binding.map.setMultiTouchControls(true) + binding.map.isTilesScaledToDpi = true + + locationOverlay = MyLocationNewOverlay(GpsMyLocationProvider(context), binding.map) + locationOverlay.enableMyLocation() + locationOverlay.setPersonHotspot(PERSON_HOT_SPOT_X, PERSON_HOT_SPOT_Y) + locationOverlay.setPersonIcon( + DisplayUtils.getBitmap( + ResourcesCompat.getDrawable(resources!!, R.drawable.current_location_circle, null)!! + ) + ) + binding.map.overlays.add(locationOverlay) + + val mapController = binding.map.controller + + if (geocodingResult != null) { + mapController.setZoom(ZOOM_LEVEL_RECEIVED_RESULT) + } else { + mapController.setZoom(ZOOM_LEVEL_DEFAULT) + } + + if (mapCenterLat != 0.0 && mapCenterLon != 0.0) { + mapController.setCenter(GeoPoint(mapCenterLat, mapCenterLon)) + } + + val zoomToCurrentPositionOnFirstFix = geocodingResult == null && moveToCurrentLocation + locationOverlay.runOnFirstFix { + if (locationOverlay.myLocation != null) { + myLocation = locationOverlay.myLocation + if (zoomToCurrentPositionOnFirstFix) { + runOnUiThread { + mapController.setZoom(ZOOM_LEVEL_DEFAULT) + mapController.setCenter(myLocation) + } + } + } else { + // locationOverlay.myLocation was null. might be an osmdroid bug? + // However that seems to be okay because runOnFirstFix is called twice somehow and the second time + // locationOverlay.myLocation is not null. + } + } + + geocodingResult?.let { + if (it.lat != COORDINATE_ZERO && it.lon != COORDINATE_ZERO) { + mapController.setCenter(GeoPoint(it.lat, it.lon)) + } + } + + binding.centerMapButton.setOnClickListener { + if (myLocation.latitude == COORDINATE_ZERO && myLocation.longitude == COORDINATE_ZERO) { + Snackbar.make( + binding.root, + context.getString(R.string.nc_location_unknown), + Snackbar.LENGTH_LONG + ).show() + } else { + mapController.animateTo(myLocation) + moveToCurrentLocation = true + } + } + + binding.map.addMapListener( + delayedMapListener() + ) + } + + private fun delayedMapListener() = + DelayedMapListener( + object : MapListener { + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onScroll(paramScrollEvent: ScrollEvent): Boolean { + try { + when { + moveToCurrentLocation -> { + setLocationDescription(isGpsLocation = true, isGeocodedResult = false) + moveToCurrentLocation = false + } + + geocodingResult != null -> { + binding.shareLocation.isClickable = true + setLocationDescription(isGpsLocation = false, isGeocodedResult = true) + geocodingResult = null + } + + else -> { + binding.shareLocation.isClickable = true + setLocationDescription(isGpsLocation = false, isGeocodedResult = false) + } + } + } catch (e: NullPointerException) { + Log.d(TAG, "UI already closed") + } + + readyToShareLocation = true + return true + } + + override fun onZoom(event: ZoomEvent): Boolean = false + } + ) + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun requestLocationUpdates() { + try { + when { + locationManager!!.isProviderEnabled(LocationManager.NETWORK_PROVIDER) -> { + locationManager!!.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + MIN_LOCATION_UPDATE_TIME, + MIN_LOCATION_UPDATE_DISTANCE, + this + ) + } + + locationManager!!.isProviderEnabled(LocationManager.GPS_PROVIDER) -> { + locationManager!!.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + MIN_LOCATION_UPDATE_TIME, + MIN_LOCATION_UPDATE_DISTANCE, + this + ) + Log.d(TAG, "LocationManager.NETWORK_PROVIDER falling back to LocationManager.GPS_PROVIDER") + } + + else -> { + Log.e( + TAG, + "Error requesting location updates. Probably this is a phone without google services" + + " and there is no alternative like UnifiedNlp installed. Furthermore no GPS is " + + "supported." + ) + Snackbar.make(binding.root, context.getString(R.string.nc_location_unknown), Snackbar.LENGTH_LONG) + .show() + } + } + } catch (e: SecurityException) { + Log.e(TAG, "Error when requesting location updates. Permissions may be missing.", e) + Snackbar.make(binding.root, context.getString(R.string.nc_location_unknown), Snackbar.LENGTH_LONG).show() + } catch (e: Exception) { + Log.e(TAG, "Error when requesting location updates.", e) + Snackbar.make(binding.root, context.getString(R.string.nc_common_error_sorry), Snackbar.LENGTH_LONG).show() + } + } + + private fun setLocationDescription(isGpsLocation: Boolean, isGeocodedResult: Boolean) { + when { + isGpsLocation -> { + binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_current_location) + binding.placeName.visibility = View.GONE + binding.placeName.text = "" + } + + isGeocodedResult -> { + binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_this_location) + binding.placeName.visibility = View.VISIBLE + binding.placeName.text = geocodingResult?.displayName + } + + else -> { + binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_this_location) + binding.placeName.visibility = View.GONE + binding.placeName.text = "" + } + } + } + + private fun shareLocation(selectedLat: Double?, selectedLon: Double?, locationName: String?) { + if (selectedLat != null || selectedLon != null) { + val name = locationName + if (name.isNullOrEmpty()) { + initGeocoder() + searchPlaceNameForCoordinates(selectedLat!!, selectedLon!!) + } else { + executeShareLocation(selectedLat, selectedLon, locationName) + } + } + } + + private fun executeShareLocation(selectedLat: Double?, selectedLon: Double?, locationName: String?) { + binding.roundedImageView.visibility = View.GONE + binding.sendingLocationProgressbar.visibility = View.VISIBLE + + val objectId = "geo:$selectedLat,$selectedLon" + + var locationNameToShare = locationName + if (locationNameToShare.isNullOrBlank()) { + locationNameToShare = resources.getString(R.string.nc_shared_location) + } + + val metaData: String = + "{\"type\":\"geo-location\",\"id\":\"geo:$selectedLat,$selectedLon\",\"latitude\":\"$selectedLat\"," + + "\"longitude\":\"$selectedLon\",\"name\":\"$locationNameToShare\"}" + + val currentUser = currentUserProvider.currentUser.blockingGet() + + ncApi.sendLocation( + ApiUtils.getCredentials(currentUser.username, currentUser.token), + ApiUtils.getUrlToSendLocation(chatApiVersion, currentUser.baseUrl!!, roomToken), + "geo-location", + objectId, + metaData + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(t: GenericOverall) { + finish() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "error when trying to share location", e) + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + finish() + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun isLocationPermissionsGranted(): Boolean { + fun isCoarseLocationGranted(): Boolean = + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PermissionChecker.PERMISSION_GRANTED + + fun isFineLocationGranted(): Boolean = + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PermissionChecker.PERMISSION_GRANTED + + return isCoarseLocationGranted() && isFineLocationGranted() + } + + private fun requestLocationPermissions() { + requestPermissions( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ), + REQUEST_PERMISSIONS_REQUEST_CODE + ) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + fun areAllGranted(grantResults: IntArray): Boolean { + grantResults.forEach { + if (it == PackageManager.PERMISSION_DENIED) return false + } + return grantResults.isNotEmpty() + } + + if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE && areAllGranted(grantResults)) { + initMap() + } else { + Snackbar.make( + binding.root, + context!!.getString(R.string.nc_location_permission_required), + Snackbar.LENGTH_LONG + ).show() + } + } + + private fun initGeocoder() { + val baseUrl = context!!.getString(R.string.osm_geocoder_url) + val email = context!!.getString(R.string.osm_geocoder_contact) + nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email) + } + + private fun searchPlaceNameForCoordinates(lat: Double, lon: Double): Boolean { + CoroutineScope(Dispatchers.IO).launch { + executeGeocodingRequest(lat, lon) + } + return true + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private suspend fun executeGeocodingRequest(lat: Double, lon: Double) { + var address: Address? = null + try { + address = nominatimClient!!.getAddress(lon, lat) + } catch (e: Exception) { + Log.e(TAG, "Failed to get geocoded addresses", e) + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + updateResultOnMainThread(lat, lon, address?.displayName) + } + + private suspend fun updateResultOnMainThread(lat: Double, lon: Double, addressName: String?) { + withContext(Dispatchers.Main) { + executeShareLocation(lat, lon, addressName) + } + } + + override fun onLocationChanged(location: Location) { + myLocation = GeoPoint(location) + } + + @Deprecated("Deprecated. This callback will never be invoked on Android Q and above.") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { + // empty + } + + override fun onProviderEnabled(provider: String) { + // empty + } + + override fun onProviderDisabled(provider: String) { + // empty + } + + companion object { + private val TAG = LocationPickerActivity::class.java.simpleName + private const val REQUEST_PERMISSIONS_REQUEST_CODE = 1 + private const val PERSON_HOT_SPOT_X: Float = 20.0F + private const val PERSON_HOT_SPOT_Y: Float = 20.0F + private const val ZOOM_LEVEL_RECEIVED_RESULT: Double = 14.0 + private const val ZOOM_LEVEL_DEFAULT: Double = 14.0 + private const val COORDINATE_ZERO: Double = 0.0 + private const val MIN_LOCATION_UPDATE_TIME: Long = 30 * 1000L + private const val MIN_LOCATION_UPDATE_DISTANCE: Float = 0f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/lock/LockedActivity.kt b/app/src/main/java/com/nextcloud/talk/lock/LockedActivity.kt new file mode 100644 index 0000000..5906bbd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/lock/LockedActivity.kt @@ -0,0 +1,150 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.lock + +import android.app.Activity +import android.app.KeyguardManager +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricPrompt +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.ActivityLockedBinding +import com.nextcloud.talk.utils.BrandingUtils +import com.nextcloud.talk.utils.SecurityUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class LockedActivity : AppCompatActivity() { + + @Inject + lateinit var context: Context + + @Inject + lateinit var appPreferences: AppPreferences + + private val startForCredentialsResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult + () + ) { + onConfirmDeviceCredentials(it) + } + + private lateinit var binding: ActivityLockedBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityLockedBinding.inflate(layoutInflater) + setContentView(binding.root) + } + + override fun onResume() { + super.onResume() + hideLogoForBrandedClients() + + binding.unlockContainer.setOnClickListener { + checkIfWeAreSecure() + } + checkIfWeAreSecure() + } + + private fun hideLogoForBrandedClients() { + if (!BrandingUtils.isOriginalNextcloudClient(applicationContext)) { + binding.appLogo.visibility = View.GONE + } + } + + private fun checkIfWeAreSecure() { + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager? + if (keyguardManager?.isKeyguardSecure == true && appPreferences.isScreenLocked) { + if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)) { + Log.d(TAG, "showBiometricDialog because 'we are NOT authenticated'...") + showBiometricDialog() + } else { + finish() + } + } + } + + private fun showBiometricDialog() { + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle( + String.format( + context.getString(R.string.nc_biometric_unlock), + context.getString(R.string.nc_app_product_name) + ) + ) + .setNegativeButtonText(context.getString(R.string.nc_cancel)) + .build() + val executor: Executor = Executors.newSingleThreadExecutor() + val biometricPrompt = BiometricPrompt( + this, + executor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Log.d(TAG, "Fingerprint recognised successfully") + finish() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.d(TAG, "Fingerprint not recognised") + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + showAuthenticationScreen() + } + } + ) + val cryptoObject = SecurityUtils.getCryptoObject() + if (cryptoObject != null) { + biometricPrompt.authenticate(promptInfo, cryptoObject) + } else { + biometricPrompt.authenticate(promptInfo) + } + } + + private fun showAuthenticationScreen() { + Log.d(TAG, "showAuthenticationScreen") + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager? + val intent = keyguardManager?.createConfirmDeviceCredentialIntent(null, null) + if (intent != null) { + startForCredentialsResult.launch(intent) + } + } + + private fun onConfirmDeviceCredentials(result: ActivityResult) { + if (result.resultCode == Activity.RESULT_OK) { + if ( + SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout) + ) { + finish() + } + } else { + Log.d(TAG, "Authorization failed") + } + } + + companion object { + private val TAG = LockedActivity::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt new file mode 100644 index 0000000..bf87be3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt @@ -0,0 +1,248 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.messagesearch + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.adapters.items.LoadMoreResultsItem +import com.nextcloud.talk.adapters.items.MessageResultItem +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.conversationlist.ConversationsListActivity +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityMessageSearchBinding +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.rx.SearchViewObservable.Companion.observeSearchView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.viewholders.FlexibleViewHolder +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class MessageSearchActivity : BaseActivity() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + private lateinit var binding: ActivityMessageSearchBinding + private lateinit var searchView: SearchView + + private lateinit var user: User + + private lateinit var viewModel: MessageSearchViewModel + + private var searchViewDisposable: Disposable? = null + private var adapter: FlexibleAdapter>? = null + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + setResult(Activity.RESULT_CANCELED) + finish() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityMessageSearchBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + initSystemBars() + + viewModel = ViewModelProvider(this, viewModelFactory)[MessageSearchViewModel::class.java] + user = currentUserProvider.currentUser.blockingGet() + val roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!! + viewModel.initialize(roomToken) + setupStateObserver() + + binding.swipeRefreshLayout.setOnRefreshListener { + viewModel.refresh(searchView.query?.toString()) + } + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + private fun setupActionBar() { + setSupportActionBar(binding.messageSearchToolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + val conversationName = intent.getStringExtra(BundleKeys.KEY_CONVERSATION_NAME) + supportActionBar?.title = conversationName + viewThemeUtils.material.themeToolbar(binding.messageSearchToolbar) + } + + private fun setupStateObserver() { + viewModel.state.observe(this) { state -> + when (state) { + MessageSearchViewModel.InitialState -> showInitial() + MessageSearchViewModel.EmptyState -> showEmpty() + is MessageSearchViewModel.LoadedState -> showLoaded(state) + MessageSearchViewModel.LoadingState -> showLoading() + MessageSearchViewModel.ErrorState -> showError() + is MessageSearchViewModel.FinishedState -> onFinish() + } + } + } + + private fun showError() { + displayLoading(false) + Snackbar.make(binding.root, "Error while searching", Snackbar.LENGTH_SHORT).show() + } + + private fun showLoading() { + displayLoading(true) + } + + private fun displayLoading(loading: Boolean) { + binding.swipeRefreshLayout.isRefreshing = loading + } + + private fun showLoaded(state: MessageSearchViewModel.LoadedState) { + displayLoading(false) + binding.emptyContainer.emptyListView.visibility = View.GONE + binding.messageSearchRecycler.visibility = View.VISIBLE + setAdapterItems(state) + } + + private fun setAdapterItems(state: MessageSearchViewModel.LoadedState) { + val loadMoreItems = if (state.hasMore) { + listOf(LoadMoreResultsItem) + } else { + emptyList() + } + val newItems = + state.results.map { MessageResultItem(this, user, it, false, viewThemeUtils) } + loadMoreItems + + if (adapter != null) { + adapter!!.updateDataSet(newItems) + } else { + createAdapter(newItems) + } + } + + private fun createAdapter(items: List>) { + adapter = FlexibleAdapter(items) + binding.messageSearchRecycler.adapter = adapter + adapter!!.addListener(object : FlexibleAdapter.OnItemClickListener { + override fun onItemClick(view: View?, position: Int): Boolean { + val item = adapter!!.getItem(position) + when (item) { + is LoadMoreResultsItem -> { + viewModel.loadMore() + } + is MessageResultItem -> { + viewModel.selectMessage(item.messageEntry) + } + } + return false + } + }) + } + + private fun onFinish() { + val state = viewModel.state.value + if (state is MessageSearchViewModel.FinishedState) { + val resultIntent = Intent().apply { + putExtra(RESULT_KEY_MESSAGE_ID, state.selectedMessageId) + } + setResult(Activity.RESULT_OK, resultIntent) + finish() + } + } + + private fun showInitial() { + displayLoading(false) + binding.messageSearchRecycler.visibility = View.GONE + binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_typing) + binding.emptyContainer.emptyListView.visibility = View.VISIBLE + } + + private fun showEmpty() { + displayLoading(false) + binding.messageSearchRecycler.visibility = View.GONE + binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_empty) + binding.emptyContainer.emptyListView.visibility = View.VISIBLE + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_search, menu) + return true + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + val menuItem = menu.findItem(R.id.action_search) + searchView = menuItem.actionView as SearchView + setupSearchView() + menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + searchView.requestFocus() + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { + onBackPressedDispatcher.onBackPressed() + return false + } + }) + menuItem.expandActionView() + return true + } + + private fun setupSearchView() { + searchView.queryHint = getString(R.string.message_search_hint) + searchViewDisposable = observeSearchView(searchView) + .debounce { query -> + when { + TextUtils.isEmpty(query) -> Observable.empty() + else -> Observable.timer( + ConversationsListActivity.SEARCH_DEBOUNCE_INTERVAL_MS.toLong(), + TimeUnit.MILLISECONDS + ) + } + } + .distinctUntilChanged() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { newText -> viewModel.onQueryTextChange(newText) } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + android.R.id.home -> { + onBackPressedDispatcher.onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + + override fun onDestroy() { + super.onDestroy() + searchViewDisposable?.dispose() + } + + companion object { + const val RESULT_KEY_MESSAGE_ID = "MessageSearchActivity.result.message" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchHelper.kt b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchHelper.kt new file mode 100644 index 0000000..8d7e021 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchHelper.kt @@ -0,0 +1,98 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.messagesearch + +import android.util.Log +import com.nextcloud.talk.models.domain.SearchMessageEntry +import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository +import io.reactivex.Observable +import io.reactivex.disposables.Disposable + +class MessageSearchHelper @JvmOverloads constructor( + private val unifiedSearchRepository: UnifiedSearchRepository, + private val fromRoom: String? = null +) { + + data class MessageSearchResults(val messages: List, val hasMore: Boolean) + + private var unifiedSearchDisposable: Disposable? = null + private var previousSearch: String? = null + private var previousCursor: Int = 0 + private var previousResults: List = emptyList() + + fun startMessageSearch(search: String): Observable { + resetCachedData() + return doSearch(search) + } + + fun loadMore(): Observable? { + previousSearch?.let { + return doSearch(it, previousCursor) + } + return null + } + + fun cancelSearch() { + disposeIfPossible() + } + + private fun doSearch(search: String, cursor: Int = 0): Observable { + disposeIfPossible() + return searchCall(search, cursor) + .map { results -> + previousSearch = search + previousCursor = results.cursor + previousResults = previousResults + results.entries + MessageSearchResults(previousResults, results.hasMore) + } + .doOnSubscribe { + unifiedSearchDisposable = it + } + .doOnError { throwable -> + Log.e(TAG, "message search - ERROR", throwable) + resetCachedData() + disposeIfPossible() + } + .doOnComplete(this::disposeIfPossible) + } + + private fun searchCall( + search: String, + cursor: Int + ): Observable> = + when { + fromRoom != null -> { + unifiedSearchRepository.searchInRoom( + roomToken = fromRoom, + searchTerm = search, + cursor = cursor + ) + } + else -> { + unifiedSearchRepository.searchMessages( + searchTerm = search, + cursor = cursor + ) + } + } + + private fun resetCachedData() { + previousSearch = null + previousCursor = 0 + previousResults = emptyList() + } + + private fun disposeIfPossible() { + unifiedSearchDisposable?.dispose() + unifiedSearchDisposable = null + } + + companion object { + private val TAG = MessageSearchHelper::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt new file mode 100644 index 0000000..e84f035 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt @@ -0,0 +1,105 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.messagesearch + +import android.annotation.SuppressLint +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.models.domain.SearchMessageEntry +import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +/** + * Install PlantUML plugin to render this state diagram + * @startuml + * hide empty description + * [*] --> InitialState + * InitialState --> LoadingState + * LoadingState --> EmptyState + * LoadingState --> LoadedState + * LoadingState --> LoadingState + * LoadedState --> LoadingState + * EmptyState --> LoadingState + * LoadingState --> ErrorState + * ErrorState --> LoadingState + * @enduml + */ +class MessageSearchViewModel @Inject constructor(private val unifiedSearchRepository: UnifiedSearchRepository) : + ViewModel() { + + sealed class ViewState + object InitialState : ViewState() + object LoadingState : ViewState() + object EmptyState : ViewState() + object ErrorState : ViewState() + class LoadedState(val results: List, val hasMore: Boolean) : ViewState() + class FinishedState(val selectedMessageId: String) : ViewState() + + private lateinit var messageSearchHelper: MessageSearchHelper + + private val _state: MutableLiveData = MutableLiveData(InitialState) + val state: LiveData + get() = _state + + fun initialize(roomToken: String) { + messageSearchHelper = MessageSearchHelper(unifiedSearchRepository, roomToken) + } + + @SuppressLint("CheckResult") // handled by helper + fun onQueryTextChange(newText: String) { + if (newText.length >= MIN_CHARS_FOR_SEARCH) { + _state.value = LoadingState + messageSearchHelper.cancelSearch() + messageSearchHelper.startMessageSearch(newText) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::onReceiveResults, this::onError) + } + } + + @SuppressLint("CheckResult") // handled by helper + fun loadMore() { + _state.value = LoadingState + messageSearchHelper.cancelSearch() + messageSearchHelper.loadMore() + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(this::onReceiveResults) + } + + private fun onReceiveResults(results: MessageSearchHelper.MessageSearchResults) { + if (results.messages.isEmpty()) { + _state.value = EmptyState + } else { + _state.value = LoadedState(results.messages, results.hasMore) + } + } + + private fun onError(throwable: Throwable) { + Log.e(TAG, "onError:", throwable) + messageSearchHelper.cancelSearch() + _state.value = ErrorState + } + + fun refresh(query: String?) { + query?.let { onQueryTextChange(it) } + } + + fun selectMessage(messageEntry: SearchMessageEntry) { + _state.value = FinishedState(messageEntry.messageId!!) + } + + companion object { + private val TAG = MessageSearchViewModel::class.simpleName + private const val MIN_CHARS_FOR_SEARCH = 2 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt b/app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt new file mode 100644 index 0000000..b561f9b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.signaling.settings.FederationSettings +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ExternalSignalingServer( + @JsonField(name = ["externalSignalingServer"]) + var externalSignalingServer: String? = null, + @JsonField(name = ["externalSignalingTicket"]) + var externalSignalingTicket: String? = null, + @JsonField(name = ["federation"]) + var federation: FederationSettings? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/ImportAccount.java b/app/src/main/java/com/nextcloud/talk/models/ImportAccount.java new file mode 100644 index 0000000..778db1f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/ImportAccount.java @@ -0,0 +1,94 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models; + +import androidx.annotation.Nullable; + +public class ImportAccount { + public String username; + @Nullable + public String token; + public String baseUrl; + + public ImportAccount(String username, @Nullable String token, String baseUrl) { + this.username = username; + this.token = token; + this.baseUrl = baseUrl; + } + + public String getUsername() { + return this.username; + } + + @Nullable + public String getToken() { + return this.token; + } + + public String getBaseUrl() { + return this.baseUrl; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setToken(@Nullable String token) { + this.token = token; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ImportAccount)) { + return false; + } + final ImportAccount other = (ImportAccount) o; + if (!other.canEqual((Object) this)) { + return false; + } + final Object this$username = this.getUsername(); + final Object other$username = other.getUsername(); + if (this$username == null ? other$username != null : !this$username.equals(other$username)) { + return false; + } + final Object this$token = this.getToken(); + final Object other$token = other.getToken(); + if (this$token == null ? other$token != null : !this$token.equals(other$token)) { + return false; + } + final Object this$baseUrl = this.getBaseUrl(); + final Object other$baseUrl = other.getBaseUrl(); + + return this$baseUrl == null ? other$baseUrl == null : this$baseUrl.equals(other$baseUrl); + } + + protected boolean canEqual(final Object other) { + return other instanceof ImportAccount; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $username = this.getUsername(); + result = result * PRIME + ($username == null ? 43 : $username.hashCode()); + final Object $token = this.getToken(); + result = result * PRIME + ($token == null ? 43 : $token.hashCode()); + final Object $baseUrl = this.getBaseUrl(); + return result * PRIME + ($baseUrl == null ? 43 : $baseUrl.hashCode()); + } + + public String toString() { + return "ImportAccount(username=" + this.getUsername() + ", token=" + this.getToken() + ", baseUrl=" + this.getBaseUrl() + ")"; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/LoginData.kt b/app/src/main/java/com/nextcloud/talk/models/LoginData.kt new file mode 100644 index 0000000..a6d9d29 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/LoginData.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class LoginData(var serverUrl: String? = null, var username: String? = null, var token: String? = null) : + Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/MessageDraft.kt b/app/src/main/java/com/nextcloud/talk/models/MessageDraft.kt new file mode 100644 index 0000000..4a49339 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/MessageDraft.kt @@ -0,0 +1,57 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models + +import android.os.Parcelable +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class MessageDraft( + @JsonField(name = ["messageText"]) + var messageText: String = "", + @JsonField(name = ["messageCursor"]) + var messageCursor: Int = 0, + @JsonField(name = ["quotedJsonId"]) + var quotedJsonId: Int? = null, + @JsonField(name = ["quotedDisplayName"]) + var quotedDisplayName: String? = null, + @JsonField(name = ["quotedMessageText"]) + var quotedMessageText: String? = null, + @JsonField(name = ["quoteImageUrl"]) + var quotedImageUrl: String? = null, + @JsonField(name = ["threadTitle"]) + var threadTitle: String? = null +) : Parcelable { + constructor() : this("", 0, null, null, null, null, null) +} + +class MessageDraftConverter { + + @TypeConverter + fun fromMessageDraftToString(messageDraft: MessageDraft?): String = + if (messageDraft == null) { + "" + } else { + LoganSquare.serialize(messageDraft) + } + + @TypeConverter + fun fromStringToMessageDraft(value: String): MessageDraft? = + if (value.isBlank()) { + null + } else { + LoganSquare.parse(value, MessageDraft::class.java) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/RetrofitBucket.kt b/app/src/main/java/com/nextcloud/talk/models/RetrofitBucket.kt new file mode 100644 index 0000000..23c8d22 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/RetrofitBucket.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class RetrofitBucket(var url: String? = null, var queryMap: MutableMap? = null) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/RingtoneSettings.kt b/app/src/main/java/com/nextcloud/talk/models/RingtoneSettings.kt new file mode 100644 index 0000000..a57886b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/RingtoneSettings.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models + +import android.net.Uri +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.converters.UriTypeConverter +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class RingtoneSettings( + @JsonField(name = ["ringtoneUri"], typeConverter = UriTypeConverter::class) + var ringtoneUri: Uri? = null, + @JsonField(name = ["ringtoneName"]) + var ringtoneName: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/SignatureVerification.kt b/app/src/main/java/com/nextcloud/talk/models/SignatureVerification.kt new file mode 100644 index 0000000..3517df3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/SignatureVerification.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models + +import android.os.Parcelable +import com.nextcloud.talk.data.user.model.User +import kotlinx.parcelize.Parcelize + +@Parcelize +data class SignatureVerification(var signatureValid: Boolean = false, var user: User? = null) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/TakePictureViewModel.java b/app/src/main/java/com/nextcloud/talk/models/TakePictureViewModel.java new file mode 100644 index 0000000..e4dcd5c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/TakePictureViewModel.java @@ -0,0 +1,102 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Stefan Niedermann + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models; + +import com.nextcloud.talk.R; + +import androidx.annotation.NonNull; +import androidx.camera.core.CameraSelector; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Transformations; +import androidx.lifecycle.ViewModel; + +import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA; +import static androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA; + +public class TakePictureViewModel extends ViewModel { + + @NonNull + private CameraSelector cameraSelector = DEFAULT_BACK_CAMERA; + + @NonNull + private final MutableLiveData torchEnabled = new MutableLiveData<>(Boolean.FALSE); + + @NonNull + private final MutableLiveData lowResolutionEnabled = new MutableLiveData<>(Boolean.FALSE); + + @NonNull + private final MutableLiveData cropEnabled = new MutableLiveData<>(Boolean.FALSE); + + @NonNull + public CameraSelector getCameraSelector() { + return this.cameraSelector; + } + + public void toggleCameraSelector() { + if (this.cameraSelector == DEFAULT_BACK_CAMERA) { + this.cameraSelector = DEFAULT_FRONT_CAMERA; + if (this.torchEnabled.getValue()) { + toggleTorchEnabled(); + } + } else { + this.cameraSelector = DEFAULT_BACK_CAMERA; + } + } + + public void disableTorchIfEnabled() { + if (this.torchEnabled.getValue()) { + toggleTorchEnabled(); + } + } + + public void toggleTorchEnabled() { + //noinspection ConstantConditions + this.torchEnabled.postValue(!this.torchEnabled.getValue()); + } + + public void toggleLowResolutionEnabled() { + //noinspection ConstantConditions + this.lowResolutionEnabled.postValue(!this.lowResolutionEnabled.getValue()); + } + + public void toggleCropEnabled() { + //noinspection ConstantConditions + this.cropEnabled.postValue(!this.cropEnabled.getValue()); + } + + public LiveData isTorchEnabled() { + return this.torchEnabled; + } + + public LiveData isLowResolutionEnabled() { + return this.lowResolutionEnabled; + } + + public LiveData isCropEnabled() { + return this.cropEnabled; + } + + public LiveData getTorchToggleButtonImageResource() { + return Transformations.map(isTorchEnabled(), enabled -> enabled + ? R.drawable.ic_baseline_flash_on_24 + : R.drawable.ic_baseline_flash_off_24); + } + + public LiveData getLowResolutionToggleButtonImageResource() { + return Transformations.map(isLowResolutionEnabled(), enabled -> enabled + ? R.drawable.ic_low_quality + : R.drawable.ic_high_quality); + } + + public LiveData getCropToggleButtonImageResource() { + return Transformations.map(isCropEnabled(), enabled -> enabled + ? R.drawable.ic_crop_16_9 + : R.drawable.ic_crop_4_3); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt new file mode 100644 index 0000000..76e00f7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt @@ -0,0 +1,137 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023-2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.domain + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.MessageDraft +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant + +data class ConversationModel( + var internalId: String, + var accountId: Long, + var token: String, + var name: String, + var displayName: String, + var description: String, + var type: ConversationEnums.ConversationType, + var lastPing: Long = 0, + var participantType: Participant.ParticipantType, + var hasPassword: Boolean = false, + var sessionId: String, + var actorId: String, + var actorType: String, + var favorite: Boolean = false, + var lastActivity: Long = 0, + var unreadMessages: Int = 0, + var unreadMention: Boolean = false, + var lastMessage: ChatMessageJson? = null, + var objectType: ConversationEnums.ObjectType, + var objectId: String = "", + var notificationLevel: ConversationEnums.NotificationLevel, + var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState, + var lobbyState: ConversationEnums.LobbyState, + var lobbyTimer: Long, + var lastReadMessage: Int = 0, + var lastCommonReadMessage: Int = 0, + var hasCall: Boolean = false, + var callFlag: Int = 0, + var canStartCall: Boolean = false, + var canLeaveConversation: Boolean, + var canDeleteConversation: Boolean, + var unreadMentionDirect: Boolean, + var notificationCalls: Int, + var permissions: Int = 0, + var messageExpiration: Int = 0, + var status: String? = null, + var statusIcon: String? = null, + var statusMessage: String? = null, + var statusClearAt: Long? = 0, + var callRecording: Int = 0, + var avatarVersion: String, + var hasCustomAvatar: Boolean, + var callStartTime: Long, + var recordingConsentRequired: Int = 0, + var remoteServer: String? = null, + var remoteToken: String? = null, + var hasArchived: Boolean = false, + var hasSensitive: Boolean = false, + var hasImportant: Boolean = false, + + // attributes that don't come from API. This should be changed?! + var password: String? = null, + var messageDraft: MessageDraft? = MessageDraft() +) { + + companion object { + @Suppress("LongMethod") + fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel = + ConversationModel( + internalId = user.id!!.toString() + "@" + conversation.token, + accountId = user.id!!, + token = conversation.token, + name = conversation.name, + displayName = conversation.displayName, + description = conversation.description, + type = conversation.type.let { ConversationEnums.ConversationType.valueOf(it.name) }, + lastPing = conversation.lastPing, + participantType = conversation.participantType.let { Participant.ParticipantType.valueOf(it.name) }, + hasPassword = conversation.hasPassword, + sessionId = conversation.sessionId, + actorId = conversation.actorId, + actorType = conversation.actorType, + password = conversation.password, + favorite = conversation.favorite, + lastActivity = conversation.lastActivity, + unreadMessages = conversation.unreadMessages, + unreadMention = conversation.unreadMention, + lastMessage = conversation.lastMessage, + objectType = conversation.objectType.let { ConversationEnums.ObjectType.valueOf(it.name) }, + objectId = conversation.objectId, + notificationLevel = conversation.notificationLevel.let { + ConversationEnums.NotificationLevel.valueOf( + it.name + ) + }, + conversationReadOnlyState = conversation.conversationReadOnlyState.let { + ConversationEnums.ConversationReadOnlyState.valueOf( + it.name + ) + }, + lobbyState = conversation.lobbyState.let { ConversationEnums.LobbyState.valueOf(it.name) }, + lobbyTimer = conversation.lobbyTimer, + lastReadMessage = conversation.lastReadMessage, + lastCommonReadMessage = conversation.lastCommonReadMessage, + hasCall = conversation.hasCall, + callFlag = conversation.callFlag, + canStartCall = conversation.canStartCall, + canLeaveConversation = conversation.canLeaveConversation, + canDeleteConversation = conversation.canDeleteConversation, + unreadMentionDirect = conversation.unreadMentionDirect, + notificationCalls = conversation.notificationCalls, + permissions = conversation.permissions, + messageExpiration = conversation.messageExpiration, + status = conversation.status, + statusIcon = conversation.statusIcon, + statusMessage = conversation.statusMessage, + statusClearAt = conversation.statusClearAt, + callRecording = conversation.callRecording, + avatarVersion = conversation.avatarVersion, + hasCustomAvatar = conversation.hasCustomAvatar, + callStartTime = conversation.callStartTime, + recordingConsentRequired = conversation.recordingConsentRequired, + remoteServer = conversation.remoteServer, + remoteToken = conversation.remoteToken, + hasArchived = conversation.hasArchived, + hasSensitive = conversation.hasSensitive, + hasImportant = conversation.hasImportant + ) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ReactionAddedModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ReactionAddedModel.kt new file mode 100644 index 0000000..6d74f16 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ReactionAddedModel.kt @@ -0,0 +1,11 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.domain + +import com.nextcloud.talk.chat.data.model.ChatMessage + +data class ReactionAddedModel(var chatMessage: ChatMessage, var emoji: String, var success: Boolean) diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ReactionDeletedModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ReactionDeletedModel.kt new file mode 100644 index 0000000..04f99a7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ReactionDeletedModel.kt @@ -0,0 +1,11 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.domain + +import com.nextcloud.talk.chat.data.model.ChatMessage + +data class ReactionDeletedModel(var chatMessage: ChatMessage, var emoji: String, var success: Boolean) diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt b/app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt new file mode 100644 index 0000000..7ef317b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.domain + +data class SearchMessageEntry( + val searchTerm: String, + val thumbnailURL: String?, + val title: String, + val messageExcerpt: String, + val conversationToken: String, + val messageId: String? +) diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/StartCallRecordingModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/StartCallRecordingModel.kt new file mode 100644 index 0000000..d5ae53e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/domain/StartCallRecordingModel.kt @@ -0,0 +1,9 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.domain + +data class StartCallRecordingModel(var success: Boolean) diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/StopCallRecordingModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/StopCallRecordingModel.kt new file mode 100644 index 0000000..6b1a930 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/domain/StopCallRecordingModel.kt @@ -0,0 +1,9 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.domain + +data class StopCallRecordingModel(var success: Boolean) diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/converters/DomainEnumNotificationLevelConverter.kt b/app/src/main/java/com/nextcloud/talk/models/domain/converters/DomainEnumNotificationLevelConverter.kt new file mode 100644 index 0000000..c950b99 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/domain/converters/DomainEnumNotificationLevelConverter.kt @@ -0,0 +1,39 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Andy Scherzinger + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.domain.converters + +import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter +import com.nextcloud.talk.models.json.conversations.ConversationEnums + +class DomainEnumNotificationLevelConverter : IntBasedTypeConverter() { + override fun getFromInt(i: Int): ConversationEnums.NotificationLevel = + when (i) { + DEFAULT -> ConversationEnums.NotificationLevel.DEFAULT + ALWAYS -> ConversationEnums.NotificationLevel.ALWAYS + MENTION -> ConversationEnums.NotificationLevel.MENTION + NEVER -> ConversationEnums.NotificationLevel.NEVER + else -> ConversationEnums.NotificationLevel.DEFAULT + } + + override fun convertToInt(`object`: ConversationEnums.NotificationLevel): Int = + when (`object`) { + ConversationEnums.NotificationLevel.DEFAULT -> DEFAULT + ConversationEnums.NotificationLevel.ALWAYS -> ALWAYS + ConversationEnums.NotificationLevel.MENTION -> MENTION + ConversationEnums.NotificationLevel.NEVER -> NEVER + else -> DEFAULT + } + + companion object { + private const val DEFAULT: Int = 0 + private const val ALWAYS: Int = 1 + private const val MENTION: Int = 2 + private const val NEVER: Int = 3 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/AnyParceler.kt b/app/src/main/java/com/nextcloud/talk/models/json/AnyParceler.kt new file mode 100644 index 0000000..aa59d2c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/AnyParceler.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json + +import android.os.Parcel +import kotlinx.parcelize.Parceler + +class AnyParceler : Parceler { + override fun create(parcel: Parcel): Any? = parcel.readValue(Any::class.java.getClassLoader()) + + override fun Any?.write(parcel: Parcel, flags: Int) { + parcel.writeValue(parcel) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOCS.kt new file mode 100644 index 0000000..8d60b82 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOCS.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.autocomplete + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class AutocompleteOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOverall.kt new file mode 100644 index 0000000..655627e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.autocomplete + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class AutocompleteOverall( + @JsonField(name = ["ocs"]) + var ocs: AutocompleteOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteUser.kt b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteUser.kt new file mode 100644 index 0000000..f487ce0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteUser.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.autocomplete + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class AutocompleteUser( + @JsonField(name = ["id"]) + var id: String?, + @JsonField(name = ["label"]) + var label: String?, + @JsonField(name = ["source"]) + var source: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt new file mode 100644 index 0000000..c0cad49 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/Capabilities.kt @@ -0,0 +1,36 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Capabilities( + @JsonField(name = ["core"]) + var coreCapability: CoreCapability?, + @JsonField(name = ["spreed"]) + var spreedCapability: SpreedCapability?, + @JsonField(name = ["notifications"]) + var notificationsCapability: NotificationsCapability?, + @JsonField(name = ["theming"]) + var themingCapability: ThemingCapability?, + @JsonField(name = ["external"]) + var externalCapability: HashMap>?, + @JsonField(name = ["provisioning_api"]) + var provisioningCapability: ProvisioningCapability?, + @JsonField(name = ["user_status"]) + var userStatusCapability: UserStatusCapability? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.kt new file mode 100644 index 0000000..4eb396c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class CapabilitiesList( + @JsonField(name = ["version"]) + var serverVersion: ServerVersion?, + @JsonField(name = ["capabilities"]) + var capabilities: Capabilities? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOCS.kt new file mode 100644 index 0000000..aa00d91 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOCS.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class CapabilitiesOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: CapabilitiesList? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOverall.kt new file mode 100644 index 0000000..a3a4bc2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class CapabilitiesOverall( + @JsonField(name = ["ocs"]) + var ocs: CapabilitiesOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CoreCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CoreCapability.kt new file mode 100644 index 0000000..59f85bc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CoreCapability.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class CoreCapability( + @JsonField(name = ["pollinterval"]) + var pollInterval: Int?, + @JsonField(name = ["webdav-root"]) + var webdavRoot: String?, + @JsonField(name = ["reference-api"]) + var referenceApi: String?, + @JsonField(name = ["reference-regex"]) + var referenceRegex: String?, + @JsonField(name = ["mod-rewrite-working"]) + var modRewriteWorking: Boolean? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/NotificationsCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/NotificationsCapability.kt new file mode 100644 index 0000000..957abe9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/NotificationsCapability.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class NotificationsCapability( + @JsonField(name = ["ocs-endpoints"]) + var features: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt new file mode 100644 index 0000000..0cd08fe --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt @@ -0,0 +1,26 @@ + +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2021 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class ProvisioningCapability( + @JsonField(name = ["AccountPropertyScopesVersion"]) + var accountPropertyScopesVersion: Int? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOCS.kt new file mode 100644 index 0000000..b34d8ec --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOCS.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class RoomCapabilitiesOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: SpreedCapability? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOverall.kt new file mode 100644 index 0000000..0a99d3e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/RoomCapabilitiesOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class RoomCapabilitiesOverall( + @JsonField(name = ["ocs"]) + var ocs: RoomCapabilitiesOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ServerVersion.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ServerVersion.kt new file mode 100644 index 0000000..f65964b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ServerVersion.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ServerVersion( + @JsonField(name = ["major"]) + var major: Int = 0, + @JsonField(name = ["minor"]) + var minor: Int = 0, + @JsonField(name = ["micro"]) + var micro: Int = 0, + @JsonField(name = ["string"]) + var versionString: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(0, 0, 0, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt new file mode 100644 index 0000000..77009e8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt @@ -0,0 +1,40 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject + +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class SpreedCapability( + @JsonField(name = ["features"]) + var features: List?, + @JsonField(name = ["config"]) + var config: HashMap< + String, + HashMap< + String, + @RawValue + @Contextual + Any + > + >?, + @JsonField(name = ["version"]) + var version: String +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, "") +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ThemingCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ThemingCapability.kt new file mode 100644 index 0000000..13eae1a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ThemingCapability.kt @@ -0,0 +1,46 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class ThemingCapability( + @JsonField(name = ["name"]) + var name: String?, + @JsonField(name = ["url"]) + var url: String?, + @JsonField(name = ["slogan"]) + var slogan: String?, + @JsonField(name = ["color"]) + var color: String?, + @JsonField(name = ["color-text"]) + var colorText: String?, + @JsonField(name = ["color-element"]) + var colorElement: String?, + @JsonField(name = ["color-element-bright"]) + var colorElementBright: String?, + @JsonField(name = ["color-element-dark"]) + var colorElementDark: String?, + @JsonField(name = ["logo"]) + var logo: String?, + @JsonField(name = ["background"]) + var background: String?, + @JsonField(name = ["background-plain"]) + var backgroundPlain: Boolean?, + @JsonField(name = ["background-default"]) + var backgroundDefault: Boolean? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null, null, null, null, null, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt new file mode 100644 index 0000000..1ccc07f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.capabilities + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class UserStatusCapability( + @JsonField(name = ["enabled"]) + var enabled: Boolean, + @JsonField(name = ["restore"]) + var restore: Boolean, + @JsonField(name = ["supports_emoji"]) + var supportsEmoji: Boolean +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(false, false, false) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt new file mode 100644 index 0000000..a845b2a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessageJson.kt @@ -0,0 +1,56 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.chat + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType +import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ChatMessageJson( + @JsonField(name = ["id"]) var id: Long = 0, + @JsonField(name = ["token"]) var token: String? = null, + @JsonField(name = ["threadId"]) var threadId: Long? = null, + + // Be aware that variables with "is" at the beginning will lead to the error: + // "@JsonField annotation can only be used on private fields if both getter and setter are present." + // Instead, name it with "has" at the beginning: isThread -> hasThread + @JsonField(name = ["isThread"]) var hasThread: Boolean = false, + @JsonField(name = ["actorType"]) var actorType: String? = null, + @JsonField(name = ["actorId"]) var actorId: String? = null, + @JsonField(name = ["actorDisplayName"]) var actorDisplayName: String? = null, + @JsonField(name = ["timestamp"]) var timestamp: Long = 0, + @JsonField(name = ["message"]) var message: String? = null, + + @JsonField(name = ["messageParameters"]) + var messageParameters: HashMap>? = null, + + @JsonField(name = ["systemMessage"], typeConverter = EnumSystemMessageTypeConverter::class) + var systemMessageType: SystemMessageType? = null, + + @JsonField(name = ["isReplyable"]) var replyable: Boolean = false, + @JsonField(name = ["parent"]) var parentMessage: ChatMessageJson? = null, + @JsonField(name = ["messageType"]) var messageType: String? = null, + @JsonField(name = ["reactions"]) var reactions: LinkedHashMap? = null, + @JsonField(name = ["reactionsSelf"]) var reactionsSelf: ArrayList? = null, + @JsonField(name = ["expirationTimestamp"]) var expirationTimestamp: Int = 0, + @JsonField(name = ["markdown"]) var renderMarkdown: Boolean? = null, + @JsonField(name = ["lastEditActorDisplayName"]) var lastEditActorDisplayName: String? = null, + @JsonField(name = ["lastEditActorId"]) var lastEditActorId: String? = null, + @JsonField(name = ["lastEditActorType"]) var lastEditActorType: String? = null, + @JsonField(name = ["lastEditTimestamp"]) var lastEditTimestamp: Long? = 0, + @JsonField(name = ["deleted"]) var deleted: Boolean = false, + @JsonField(name = ["referenceId"]) var referenceId: String? = null, + @JsonField(name = ["silent"]) var silent: Boolean = false, + @JsonField(name = ["threadTitle"]) var threadTitle: String? = null, + @JsonField(name = ["threadReplies"]) var threadReplies: Int? = 0 +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCS.kt new file mode 100644 index 0000000..d8f27ab --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.chat + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ChatOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: List? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.kt new file mode 100644 index 0000000..63b52c5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCSSingleMessage.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.chat + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ChatOCSSingleMessage( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: ChatMessageJson? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverall.kt new file mode 100644 index 0000000..7532944 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.chat + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ChatOverall( + @JsonField(name = ["ocs"]) + var ocs: ChatOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverallSingleMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverallSingleMessage.kt new file mode 100644 index 0000000..049d045 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverallSingleMessage.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.chat + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ChatOverallSingleMessage( + @JsonField(name = ["ocs"]) + var ocs: ChatOCSSingleMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.kt new file mode 100644 index 0000000..0c8ba73 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOCS.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.chat + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ChatShareOCS( + @JsonField(name = ["data"]) + var data: HashMap? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverall.kt new file mode 100644 index 0000000..db18ffa --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.chat + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ChatShareOverall( + @JsonField(name = ["ocs"]) + var ocs: ChatShareOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverviewOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverviewOCS.kt new file mode 100644 index 0000000..2cae74c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverviewOCS.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.chat + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.AnyParceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler +import java.util.HashMap + +@Parcelize +@JsonObject +@TypeParceler +data class ChatShareOverviewOCS( + @JsonField(name = ["data"]) + var data: HashMap>? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverviewOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverviewOverall.kt new file mode 100644 index 0000000..a146958 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatShareOverviewOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.chat + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ChatShareOverviewOverall( + @JsonField(name = ["ocs"]) + var ocs: ChatShareOverviewOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt new file mode 100644 index 0000000..bc674ad --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt @@ -0,0 +1,61 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.chat + +@Suppress("UtilityClassWithPublicConstructor") +class ChatUtils { + companion object { + fun getParsedMessage( + message: String?, + messageParameters: HashMap>? + ): String? { + if (messageParameters != null && messageParameters.size > 0) { + return parse(messageParameters, message) + } + return message + } + + @Suppress("Detekt.ComplexMethod", "Detekt.ComplexCondition") + private fun parse(messageParameters: HashMap>, message: String?): String? { + var resultMessage = message + for (key in messageParameters.keys) { + val individualHashMap = messageParameters[key] + + if (individualHashMap != null) { + val type = individualHashMap["type"] + resultMessage = if (type == "user" || + type == "guest" || + type == "call" || + type == "email" || + type == "user-group" || + type == "circle" + ) { + resultMessage?.replace("{$key}", "@" + individualHashMap["name"]) + } else if (type == "geo-location") { + individualHashMap["name"] + } else if (individualHashMap.containsKey("link")) { + if (type == "file") { + resultMessage?.replace("{$key}", individualHashMap["name"].toString()) + } else { + individualHashMap["name"]?.let { + resultMessage?.replace( + "{$key}", + individualHashMap["name"]!! + ) + } + } + } else { + individualHashMap["name"]?.let { resultMessage?.replace("{$key}", it) } + } + } + } + return resultMessage + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt new file mode 100644 index 0000000..40a1e28 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ReadStatus.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.chat + +enum class ReadStatus { + NONE, + SENT, + READ +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt new file mode 100644 index 0000000..c6750a2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt @@ -0,0 +1,175 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.conversations + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.models.json.converters.ConversationObjectTypeConverter +import com.nextcloud.talk.models.json.converters.EnumLobbyStateConverter +import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter +import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter +import com.nextcloud.talk.models.json.converters.EnumReadOnlyConversationConverter +import com.nextcloud.talk.models.json.converters.EnumRoomTypeConverter +import com.nextcloud.talk.models.json.participants.Participant.ParticipantType +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Conversation( + @JsonField(name = ["token"]) + var token: String = "", + + @JsonField(name = ["name"]) + var name: String = "", + + @JsonField(name = ["displayName"]) + var displayName: String = "", + + @JsonField(name = ["description"]) + var description: String = "", + + @JsonField(name = ["type"], typeConverter = EnumRoomTypeConverter::class) + var type: ConversationEnums.ConversationType = ConversationEnums.ConversationType.DUMMY, + + @JsonField(name = ["lastPing"]) + var lastPing: Long = 0, + + @JsonField(name = ["participantType"], typeConverter = EnumParticipantTypeConverter::class) + var participantType: ParticipantType = ParticipantType.DUMMY, + + @JsonField(name = ["hasPassword"]) + var hasPassword: Boolean = false, + + @JsonField(name = ["sessionId"]) + var sessionId: String = "0", + + @JsonField(name = ["actorId"]) + var actorId: String = "", + + @JsonField(name = ["actorType"]) + var actorType: String = "", + + // check if this can be removed. Doesn't belong to api-response but is used internally? + var password: String? = null, + + @JsonField(name = ["isFavorite"]) + var favorite: Boolean = false, + + @JsonField(name = ["lastActivity"]) + var lastActivity: Long = 0, + + @JsonField(name = ["unreadMessages"]) + var unreadMessages: Int = 0, + + @JsonField(name = ["unreadMention"]) + var unreadMention: Boolean = false, + + @JsonField(name = ["lastMessage"]) + var lastMessage: ChatMessageJson? = null, + + @JsonField(name = ["objectType"], typeConverter = ConversationObjectTypeConverter::class) + var objectType: ConversationEnums.ObjectType = ConversationEnums.ObjectType.DEFAULT, + + @JsonField(name = ["objectId"]) + var objectId: String = "", + + @JsonField(name = ["notificationLevel"], typeConverter = EnumNotificationLevelConverter::class) + var notificationLevel: ConversationEnums.NotificationLevel = ConversationEnums.NotificationLevel.DEFAULT, + + @JsonField(name = ["readOnly"], typeConverter = EnumReadOnlyConversationConverter::class) + var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState = + ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_WRITE, + + @JsonField(name = ["lobbyState"], typeConverter = EnumLobbyStateConverter::class) + var lobbyState: ConversationEnums.LobbyState = ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS, + + @JsonField(name = ["lobbyTimer"]) + var lobbyTimer: Long = 0, + + @JsonField(name = ["lastReadMessage"]) + var lastReadMessage: Int = 0, + + @JsonField(name = ["lastCommonReadMessage"]) + var lastCommonReadMessage: Int = 0, + + @JsonField(name = ["hasCall"]) + var hasCall: Boolean = false, + + @JsonField(name = ["callFlag"]) + var callFlag: Int = 0, + + @JsonField(name = ["canStartCall"]) + var canStartCall: Boolean = false, + + @JsonField(name = ["canLeaveConversation"]) + var canLeaveConversation: Boolean = true, + + @JsonField(name = ["canDeleteConversation"]) + var canDeleteConversation: Boolean = false, + + @JsonField(name = ["unreadMentionDirect"]) + var unreadMentionDirect: Boolean = false, + + @JsonField(name = ["notificationCalls"]) + var notificationCalls: Int = 0, + + @JsonField(name = ["permissions"]) + var permissions: Int = 0, + + @JsonField(name = ["messageExpiration"]) + var messageExpiration: Int = 0, + + @JsonField(name = ["status"]) + var status: String? = "", + + @JsonField(name = ["statusIcon"]) + var statusIcon: String? = "", + + @JsonField(name = ["statusMessage"]) + var statusMessage: String? = "", + + @JsonField(name = ["statusClearAt"]) + var statusClearAt: Long? = null, + + @JsonField(name = ["callRecording"]) + var callRecording: Int = 0, + + @JsonField(name = ["avatarVersion"]) + var avatarVersion: String = "", + + // Be aware that variables with "is" at the beginning will lead to the error: + // "@JsonField annotation can only be used on private fields if both getter and setter are present." + // Instead, name it with "has" at the beginning: isCustomAvatar -> hasCustomAvatar + @JsonField(name = ["isCustomAvatar"]) + var hasCustomAvatar: Boolean = false, + + @JsonField(name = ["callStartTime"]) + var callStartTime: Long = 0L, + + @JsonField(name = ["recordingConsent"]) + var recordingConsentRequired: Int = 0, + + @JsonField(name = ["remoteServer"]) + var remoteServer: String? = "", + + @JsonField(name = ["remoteToken"]) + var remoteToken: String? = "", + + @JsonField(name = ["isArchived"]) + var hasArchived: Boolean = false, + + @JsonField(name = ["isSensitive"]) + var hasSensitive: Boolean = false, + + @JsonField(name = ["isImportant"]) + var hasImportant: Boolean = false +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt new file mode 100644 index 0000000..56a8d96 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/ConversationEnums.kt @@ -0,0 +1,52 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.conversations + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +class ConversationEnums { + enum class NotificationLevel { + DEFAULT, + ALWAYS, + MENTION, + NEVER + } + + enum class LobbyState { + LOBBY_STATE_ALL_PARTICIPANTS, + LOBBY_STATE_MODERATORS_ONLY + } + + enum class ConversationReadOnlyState { + CONVERSATION_READ_WRITE, + CONVERSATION_READ_ONLY + } + + @Parcelize + enum class ConversationType : Parcelable { + DUMMY, + ROOM_TYPE_ONE_TO_ONE_CALL, + ROOM_GROUP_CALL, + ROOM_PUBLIC_CALL, + ROOM_SYSTEM, + FORMER_ONE_TO_ONE, + NOTE_TO_SELF + } + + enum class ObjectType { + DEFAULT, + SHARE_PASSWORD, + FILE, + ROOM, + EVENT, + PHONE_TEMPORARY, + PHONE_PERSIST, + INSTANT_MEETING + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomOCS.kt new file mode 100644 index 0000000..8f72d0f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.conversations + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class RoomOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: Conversation? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomOverall.kt new file mode 100644 index 0000000..9dca5ce --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.conversations + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class RoomOverall( + @JsonField(name = ["ocs"]) + var ocs: RoomOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomsOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomsOCS.kt new file mode 100644 index 0000000..a529f61 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomsOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.conversations + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class RoomsOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: List? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomsOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomsOverall.kt new file mode 100644 index 0000000..f8dce16 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/RoomsOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.conversations + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class RoomsOverall( + @JsonField(name = ["ocs"]) + var ocs: RoomsOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordData.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordData.kt new file mode 100644 index 0000000..aa6a9e8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordData.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.conversations.password + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@JsonObject +@Parcelize +data class PasswordData( + @JsonField(name = ["message"]) + var message: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordOCS.kt new file mode 100644 index 0000000..ad714af --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordOCS.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.conversations.password + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PasswordOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta? = null, + + @JsonField(name = ["data"]) + var data: PasswordData? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordOverall.kt new file mode 100644 index 0000000..ac6e042 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/password/PasswordOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.conversations.password + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PasswordOverall( + @JsonField(name = ["ocs"]) + var ocs: PasswordOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt new file mode 100644 index 0000000..b7e429f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/ConversationObjectTypeConverter.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters + +import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter +import com.nextcloud.talk.models.json.conversations.ConversationEnums + +class ConversationObjectTypeConverter : StringBasedTypeConverter() { + override fun getFromString(string: String?): ConversationEnums.ObjectType = + when (string) { + "share:password" -> ConversationEnums.ObjectType.SHARE_PASSWORD + "room" -> ConversationEnums.ObjectType.ROOM + "file" -> ConversationEnums.ObjectType.FILE + "event" -> ConversationEnums.ObjectType.EVENT + "phone_persist" -> ConversationEnums.ObjectType.PHONE_PERSIST + "phone_temporary" -> ConversationEnums.ObjectType.PHONE_TEMPORARY + "instant_meeting" -> ConversationEnums.ObjectType.INSTANT_MEETING + else -> ConversationEnums.ObjectType.DEFAULT + } + + override fun convertToString(`object`: ConversationEnums.ObjectType?): String { + if (`object` == null) { + return "" + } + + return when (`object`) { + ConversationEnums.ObjectType.SHARE_PASSWORD -> "share:password" + ConversationEnums.ObjectType.ROOM -> "room" + ConversationEnums.ObjectType.FILE -> "file" + ConversationEnums.ObjectType.EVENT -> "event" + ConversationEnums.ObjectType.PHONE_PERSIST -> "phone_persist" + ConversationEnums.ObjectType.PHONE_TEMPORARY -> "phone_temporary" + ConversationEnums.ObjectType.INSTANT_MEETING -> "instant_meeting" + else -> "" + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumActorTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumActorTypeConverter.kt new file mode 100644 index 0000000..549df58 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumActorTypeConverter.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Joas Schilling + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters + +import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.models.json.participants.Participant.ActorType.CIRCLES +import com.nextcloud.talk.models.json.participants.Participant.ActorType.DUMMY +import com.nextcloud.talk.models.json.participants.Participant.ActorType.EMAILS +import com.nextcloud.talk.models.json.participants.Participant.ActorType.FEDERATED +import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS +import com.nextcloud.talk.models.json.participants.Participant.ActorType.GUESTS +import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS +import com.nextcloud.talk.models.json.participants.Participant.ActorType.PHONES + +class EnumActorTypeConverter : StringBasedTypeConverter() { + override fun getFromString(string: String?): Participant.ActorType = + when (string) { + "emails" -> EMAILS + "groups" -> GROUPS + "guests" -> GUESTS + "users" -> USERS + "circles" -> CIRCLES + "federated_users" -> FEDERATED + "phones" -> PHONES + else -> DUMMY + } + + override fun convertToString(`object`: Participant.ActorType?): String { + if (`object` == null) { + return "" + } + + return when (`object`) { + EMAILS -> "emails" + GROUPS -> "groups" + GUESTS -> "guests" + USERS -> "users" + CIRCLES -> "circles" + FEDERATED -> "federated_users" + PHONES -> "phones" + else -> "" + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumLobbyStateConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumLobbyStateConverter.java new file mode 100644 index 0000000..51f78ce --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumLobbyStateConverter.java @@ -0,0 +1,37 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters; + +import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; +import com.nextcloud.talk.models.json.conversations.Conversation; +import com.nextcloud.talk.models.json.conversations.ConversationEnums; + +public class EnumLobbyStateConverter extends IntBasedTypeConverter { + @Override + public ConversationEnums.LobbyState getFromInt(int i) { + switch (i) { + case 0: + return ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS; + case 1: + return ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY; + default: + return ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS; + } + } + + @Override + public int convertToInt(ConversationEnums.LobbyState object) { + switch (object) { + case LOBBY_STATE_ALL_PARTICIPANTS: + return 0; + case LOBBY_STATE_MODERATORS_ONLY: + return 1; + default: + return 0; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumNotificationLevelConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumNotificationLevelConverter.java new file mode 100644 index 0000000..e38bcc6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumNotificationLevelConverter.java @@ -0,0 +1,46 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters; + +import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; +import com.nextcloud.talk.models.json.conversations.Conversation; +import com.nextcloud.talk.models.json.conversations.ConversationEnums; + +public class EnumNotificationLevelConverter extends IntBasedTypeConverter { + @Override + public ConversationEnums.NotificationLevel getFromInt(int i) { + switch (i) { + case 0: + return ConversationEnums.NotificationLevel.DEFAULT; + case 1: + return ConversationEnums.NotificationLevel.ALWAYS; + case 2: + return ConversationEnums.NotificationLevel.MENTION; + case 3: + return ConversationEnums.NotificationLevel.NEVER; + default: + return ConversationEnums.NotificationLevel.DEFAULT; + } + } + + @Override + public int convertToInt(ConversationEnums.NotificationLevel object) { + switch (object) { + case DEFAULT: + return 0; + case ALWAYS: + return 1; + case MENTION: + return 2; + case NEVER: + return 3; + default: + return 0; + } + } + +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumParticipantTypeConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumParticipantTypeConverter.java new file mode 100644 index 0000000..26d8f3c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumParticipantTypeConverter.java @@ -0,0 +1,54 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters; + +import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; +import com.nextcloud.talk.models.json.participants.Participant; + +public class EnumParticipantTypeConverter extends IntBasedTypeConverter { + @Override + public Participant.ParticipantType getFromInt(int i) { + switch (i) { + case 1: + return Participant.ParticipantType.OWNER; + case 2: + return Participant.ParticipantType.MODERATOR; + case 3: + return Participant.ParticipantType.USER; + case 4: + return Participant.ParticipantType.GUEST; + case 5: + return Participant.ParticipantType.USER_FOLLOWING_LINK; + case 6: + return Participant.ParticipantType.GUEST_MODERATOR; + default: + return Participant.ParticipantType.DUMMY; + } + } + + @Override + public int convertToInt(Participant.ParticipantType object) { + switch (object) { + case DUMMY: + return 0; + case OWNER: + return 1; + case MODERATOR: + return 2; + case USER: + return 3; + case GUEST: + return 4; + case USER_FOLLOWING_LINK: + return 5; + case GUEST_MODERATOR: + return 6; + default: + return 0; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReactionActorTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReactionActorTypeConverter.kt new file mode 100644 index 0000000..028d23f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReactionActorTypeConverter.kt @@ -0,0 +1,34 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters + +import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter +import com.nextcloud.talk.models.json.reactions.ReactionVoter.ReactionActorType.DUMMY +import com.nextcloud.talk.models.json.reactions.ReactionVoter.ReactionActorType.GUESTS +import com.nextcloud.talk.models.json.reactions.ReactionVoter.ReactionActorType.USERS +import com.nextcloud.talk.models.json.reactions.ReactionVoter + +class EnumReactionActorTypeConverter : StringBasedTypeConverter() { + override fun getFromString(string: String): ReactionVoter.ReactionActorType = + when (string) { + "guests" -> GUESTS + "users" -> USERS + else -> DUMMY + } + + override fun convertToString(`object`: ReactionVoter.ReactionActorType?): String { + if (`object` == null) { + return "" + } + + return when (`object`) { + GUESTS -> "guests" + USERS -> "users" + else -> "" + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReadOnlyConversationConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReadOnlyConversationConverter.java new file mode 100644 index 0000000..ba76f71 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReadOnlyConversationConverter.java @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters; + +import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; +import com.nextcloud.talk.models.json.conversations.Conversation; +import com.nextcloud.talk.models.json.conversations.ConversationEnums; + +public class EnumReadOnlyConversationConverter extends IntBasedTypeConverter { + @Override + public ConversationEnums.ConversationReadOnlyState getFromInt(int i) { + switch (i) { + case 0: + return ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_WRITE; + case 1: + return ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY; + default: + return ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_WRITE; + } + } + + @Override + public int convertToInt(ConversationEnums.ConversationReadOnlyState object) { + switch (object) { + case CONVERSATION_READ_WRITE: + return 0; + case CONVERSATION_READ_ONLY: + return 1; + default: + return 0; + } + } + +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumRoomTypeConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumRoomTypeConverter.java new file mode 100644 index 0000000..702e0a6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumRoomTypeConverter.java @@ -0,0 +1,54 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters; + +import com.bluelinelabs.logansquare.typeconverters.IntBasedTypeConverter; +import com.nextcloud.talk.models.json.conversations.ConversationEnums; + +public class EnumRoomTypeConverter extends IntBasedTypeConverter { + @Override + public ConversationEnums.ConversationType getFromInt(int i) { + switch (i) { + case 1: + return ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL; + case 2: + return ConversationEnums.ConversationType.ROOM_GROUP_CALL; + case 3: + return ConversationEnums.ConversationType.ROOM_PUBLIC_CALL; + case 4: + return ConversationEnums.ConversationType.ROOM_SYSTEM; + case 5: + return ConversationEnums.ConversationType.FORMER_ONE_TO_ONE; + case 6: + return ConversationEnums.ConversationType.NOTE_TO_SELF; + default: + return ConversationEnums.ConversationType.DUMMY; + } + } + + @Override + public int convertToInt(ConversationEnums.ConversationType object) { + switch (object) { + case DUMMY: + return 0; + case ROOM_TYPE_ONE_TO_ONE_CALL: + return 1; + case ROOM_GROUP_CALL: + return 2; + case ROOM_PUBLIC_CALL: + return 3; + case ROOM_SYSTEM: + return 4; + case FORMER_ONE_TO_ONE: + return 5; + case NOTE_TO_SELF: + return 6; + default: + return 0; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt new file mode 100644 index 0000000..bb3e72c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt @@ -0,0 +1,220 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters + +import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AUDIO_RECORDING_STARTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AUDIO_RECORDING_STOPPED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AVATAR_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.AVATAR_SET +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.BREAKOUT_ROOMS_STARTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.BREAKOUT_ROOMS_STOPPED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_ENDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_JOINED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_LEFT +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_MISSED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_STARTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CALL_TRIED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CIRCLE_ADDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CIRCLE_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CLEARED_CHAT +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CONVERSATION_CREATED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.CONVERSATION_RENAMED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.DESCRIPTION_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.DESCRIPTION_SET +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.DUMMY +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.FEDERATED_USER_ADDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.FEDERATED_USER_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.FILE_SHARED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GROUP_ADDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GROUP_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUESTS_ALLOWED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUESTS_DISALLOWED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUEST_MODERATOR_DEMOTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.GUEST_MODERATOR_PROMOTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LISTABLE_ALL +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LISTABLE_NONE +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LISTABLE_USERS +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LOBBY_NONE +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LOBBY_NON_MODERATORS +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.LOBBY_OPEN_TO_EVERYONE +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_ADDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_DISABLED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_EDITED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_ENABLED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MATTERBRIDGE_CONFIG_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MESSAGE_DELETED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MESSAGE_EXPIRATION_DISABLED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MESSAGE_EXPIRATION_ENABLED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MODERATOR_DEMOTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.MODERATOR_PROMOTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.OBJECT_SHARED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.PASSWORD_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.PASSWORD_SET +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.POLL_CLOSED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.POLL_VOTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.REACTION +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.REACTION_DELETED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.REACTION_REVOKED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.READ_ONLY +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.READ_ONLY_OFF +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.RECORDING_FAILED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.RECORDING_STARTED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.RECORDING_STOPPED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.USER_ADDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.USER_REMOVED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.PHONE_ADDED +import com.nextcloud.talk.chat.data.model.ChatMessage.SystemMessageType.THREAD_CREATED + +/* +* see https://nextcloud-talk.readthedocs.io/en/latest/chat/#system-messages +* +*/ +class EnumSystemMessageTypeConverter : StringBasedTypeConverter() { + @Suppress("Detekt.LongMethod") + override fun getFromString(string: String): ChatMessage.SystemMessageType = + when (string) { + "conversation_created" -> CONVERSATION_CREATED + "conversation_renamed" -> CONVERSATION_RENAMED + "description_set" -> DESCRIPTION_SET + "description_removed" -> DESCRIPTION_REMOVED + "call_started" -> CALL_STARTED + "call_joined" -> CALL_JOINED + "call_left" -> CALL_LEFT + "call_ended" -> CALL_ENDED + "call_ended_everyone" -> CALL_ENDED_EVERYONE + "call_missed" -> CALL_MISSED + "call_tried" -> CALL_TRIED + "read_only_off" -> READ_ONLY_OFF + "read_only" -> READ_ONLY + "listable_none" -> LISTABLE_NONE + "listable_users" -> LISTABLE_USERS + "listable_all" -> LISTABLE_ALL + "lobby_none" -> LOBBY_NONE + "lobby_non_moderators" -> LOBBY_NON_MODERATORS + "lobby_timer_reached" -> LOBBY_OPEN_TO_EVERYONE + "guests_allowed" -> GUESTS_ALLOWED + "guests_disallowed" -> GUESTS_DISALLOWED + "password_set" -> PASSWORD_SET + "password_removed" -> PASSWORD_REMOVED + "user_added" -> USER_ADDED + "user_removed" -> USER_REMOVED + "group_added" -> GROUP_ADDED + "group_removed" -> GROUP_REMOVED + "circle_added" -> CIRCLE_ADDED + "circle_removed" -> CIRCLE_REMOVED + "moderator_promoted" -> MODERATOR_PROMOTED + "moderator_demoted" -> MODERATOR_DEMOTED + "guest_moderator_promoted" -> GUEST_MODERATOR_PROMOTED + "guest_moderator_demoted" -> GUEST_MODERATOR_DEMOTED + "message_deleted" -> MESSAGE_DELETED + "message_edited" -> ChatMessage.SystemMessageType.MESSAGE_EDITED + "file_shared" -> FILE_SHARED + "object_shared" -> OBJECT_SHARED + "matterbridge_config_added" -> MATTERBRIDGE_CONFIG_ADDED + "matterbridge_config_edited" -> MATTERBRIDGE_CONFIG_EDITED + "matterbridge_config_removed" -> MATTERBRIDGE_CONFIG_REMOVED + "matterbridge_config_enabled" -> MATTERBRIDGE_CONFIG_ENABLED + "matterbridge_config_disabled" -> MATTERBRIDGE_CONFIG_DISABLED + "history_cleared" -> CLEARED_CHAT + "reaction" -> REACTION + "reaction_deleted" -> REACTION_DELETED + "reaction_revoked" -> REACTION_REVOKED + "poll_voted" -> POLL_VOTED + "poll_closed" -> POLL_CLOSED + "message_expiration_enabled" -> MESSAGE_EXPIRATION_ENABLED + "message_expiration_disabled" -> MESSAGE_EXPIRATION_DISABLED + "recording_started" -> RECORDING_STARTED + "recording_stopped" -> RECORDING_STOPPED + "audio_recording_started" -> AUDIO_RECORDING_STARTED + "audio_recording_stopped" -> AUDIO_RECORDING_STOPPED + "recording_failed" -> RECORDING_FAILED + "breakout_rooms_started" -> BREAKOUT_ROOMS_STARTED + "breakout_rooms_stopped" -> BREAKOUT_ROOMS_STOPPED + "avatar_set" -> AVATAR_SET + "avatar_removed" -> AVATAR_REMOVED + "federated_user_added" -> FEDERATED_USER_ADDED + "federated_user_removed" -> FEDERATED_USER_REMOVED + "phone_added" -> PHONE_ADDED + "thread_created" -> THREAD_CREATED + else -> DUMMY + } + + @Suppress("Detekt.ComplexMethod", "Detekt.LongMethod") + override fun convertToString(`object`: ChatMessage.SystemMessageType?): String = + when (`object`) { + null -> "" + CONVERSATION_CREATED -> "conversation_created" + CONVERSATION_RENAMED -> "conversation_renamed" + DESCRIPTION_REMOVED -> "description_removed" + DESCRIPTION_SET -> "description_set" + CALL_STARTED -> "call_started" + CALL_JOINED -> "call_joined" + CALL_LEFT -> "call_left" + CALL_ENDED -> "call_ended" + CALL_ENDED_EVERYONE -> "call_ended_everyone" + CALL_MISSED -> "call_missed" + CALL_TRIED -> "call_tried" + READ_ONLY_OFF -> "read_only_off" + READ_ONLY -> "read_only" + LISTABLE_NONE -> "listable_none" + LISTABLE_USERS -> "listable_users" + LISTABLE_ALL -> "listable_all" + LOBBY_NONE -> "lobby_none" + LOBBY_NON_MODERATORS -> "lobby_non_moderators" + LOBBY_OPEN_TO_EVERYONE -> "lobby_timer_reached" + GUESTS_ALLOWED -> "guests_allowed" + GUESTS_DISALLOWED -> "guests_disallowed" + PASSWORD_SET -> "password_set" + PASSWORD_REMOVED -> "password_removed" + USER_ADDED -> "user_added" + USER_REMOVED -> "user_removed" + GROUP_ADDED -> "group_added" + GROUP_REMOVED -> "group_removed" + CIRCLE_ADDED -> "circle_added" + CIRCLE_REMOVED -> "circle_removed" + MODERATOR_PROMOTED -> "moderator_promoted" + MODERATOR_DEMOTED -> "moderator_demoted" + GUEST_MODERATOR_PROMOTED -> "guest_moderator_promoted" + GUEST_MODERATOR_DEMOTED -> "guest_moderator_demoted" + MESSAGE_DELETED -> "message_deleted" + ChatMessage.SystemMessageType.MESSAGE_EDITED -> "message_edited" + FILE_SHARED -> "file_shared" + OBJECT_SHARED -> "object_shared" + MATTERBRIDGE_CONFIG_ADDED -> "matterbridge_config_added" + MATTERBRIDGE_CONFIG_EDITED -> "matterbridge_config_edited" + MATTERBRIDGE_CONFIG_REMOVED -> "matterbridge_config_removed" + MATTERBRIDGE_CONFIG_ENABLED -> "matterbridge_config_enabled" + MATTERBRIDGE_CONFIG_DISABLED -> "matterbridge_config_disabled" + CLEARED_CHAT -> "clear_history" + REACTION -> "reaction" + REACTION_DELETED -> "reaction_deleted" + REACTION_REVOKED -> "reaction_revoked" + POLL_VOTED -> "poll_voted" + POLL_CLOSED -> "poll_closed" + MESSAGE_EXPIRATION_ENABLED -> "message_expiration_enabled" + MESSAGE_EXPIRATION_DISABLED -> "message_expiration_disabled" + RECORDING_STARTED -> "recording_started" + RECORDING_STOPPED -> "recording_stopped" + AUDIO_RECORDING_STARTED -> "audio_recording_started" + AUDIO_RECORDING_STOPPED -> "audio_recording_stopped" + RECORDING_FAILED -> "recording_failed" + BREAKOUT_ROOMS_STARTED -> "breakout_rooms_started" + BREAKOUT_ROOMS_STOPPED -> "breakout_rooms_stopped" + AVATAR_SET -> "avatar_set" + AVATAR_REMOVED -> "avatar_removed" + FEDERATED_USER_ADDED -> "federated_user_added" + FEDERATED_USER_REMOVED -> "federated_user_removed" + PHONE_ADDED -> "phone_added" + THREAD_CREATED -> "thread_created" + else -> "" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/LoganSquareJodaTimeConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/LoganSquareJodaTimeConverter.java new file mode 100644 index 0000000..f65493a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/LoganSquareJodaTimeConverter.java @@ -0,0 +1,46 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2016 Touch Instinct + * SPDX-License-Identifier: Apache-2.0 + * + * This file was part of RoboSwag library. + */ +package com.nextcloud.talk.models.json.converters; + +import android.util.Log; + +import com.bluelinelabs.logansquare.typeconverters.TypeConverter; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; + +import org.joda.time.DateTime; + +import java.io.IOException; + +public class LoganSquareJodaTimeConverter implements TypeConverter { + private static final String TAG = LoganSquareJodaTimeConverter.class.getSimpleName(); + + @Override + public DateTime parse(JsonParser jsonParser) throws IOException { + final String dateString = jsonParser.getValueAsString(); + if (dateString == null) { + return null; + } + try { + return DateTime.parse(dateString); + } catch (final RuntimeException exception) { + Log.e(TAG, exception.getLocalizedMessage(), exception); + } + return null; + } + + @Override + public void serialize(DateTime object, String fieldName, boolean writeFieldNameForObject, JsonGenerator jsonGenerator) throws IOException { + if (fieldName != null) { + jsonGenerator.writeStringField(fieldName, object != null ? object.toString() : null); + } else { + jsonGenerator.writeString(object != null ? object.toString() : null); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/ObjectParcelConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/ObjectParcelConverter.java new file mode 100644 index 0000000..8e0da29 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/ObjectParcelConverter.java @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters; + +import android.os.Parcel; +import org.parceler.ParcelConverter; +import org.parceler.Parcels; + +public class ObjectParcelConverter implements ParcelConverter { + @Override + public void toParcel(Object input, Parcel parcel) { + parcel.writeParcelable(Parcels.wrap(input), 0); + } + + @Override + public Object fromParcel(Parcel parcel) { + return parcel.readParcelable(Object.class.getClassLoader()); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/ScopeConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/ScopeConverter.java new file mode 100644 index 0000000..c931919 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/ScopeConverter.java @@ -0,0 +1,33 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters; + +import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter; +import com.nextcloud.talk.models.json.userprofile.Scope; + +public class ScopeConverter extends StringBasedTypeConverter { + @Override + public Scope getFromString(String string) { + switch (string) { + case "v2-private": + return Scope.PRIVATE; + case "v2-local": + return Scope.LOCAL; + case "v2-federated": + return Scope.FEDERATED; + case "v2-published": + return Scope.PUBLISHED; + default: + return Scope.PRIVATE; + } + } + + @Override + public String convertToString(Scope scope) { + return scope.getId(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/UriTypeConverter.java b/app/src/main/java/com/nextcloud/talk/models/json/converters/UriTypeConverter.java new file mode 100644 index 0000000..10b8979 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/UriTypeConverter.java @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.converters; + +import android.net.Uri; +import android.text.TextUtils; +import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter; + +public class UriTypeConverter extends StringBasedTypeConverter { + @Override + public Uri getFromString(String string) { + if (!TextUtils.isEmpty(string)) { + return Uri.parse(string); + } else { + return null; + } + } + + @Override + public String convertToString(Uri object) { + if (object != null) { + return object.toString(); + } else { + return null; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/generic/GenericMeta.kt b/app/src/main/java/com/nextcloud/talk/models/json/generic/GenericMeta.kt new file mode 100644 index 0000000..9f06b94 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/generic/GenericMeta.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.generic + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject(serializeNullObjects = true) +data class GenericMeta( + @JsonField(name = ["status"]) + var status: String? = null, + @JsonField(name = ["statuscode"]) + var statusCode: Int = 0, + @JsonField(name = ["message"]) + var message: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, 0, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/generic/GenericOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/generic/GenericOCS.kt new file mode 100644 index 0000000..31cb934 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/generic/GenericOCS.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.generic + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class GenericOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/generic/GenericOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/generic/GenericOverall.kt new file mode 100644 index 0000000..f95bae6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/generic/GenericOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.generic + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class GenericOverall( + @JsonField(name = ["ocs"]) + var ocs: GenericOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/generic/Status.kt b/app/src/main/java/com/nextcloud/talk/models/json/generic/Status.kt new file mode 100644 index 0000000..eac506e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/generic/Status.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.generic + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Status( + @JsonField(name = ["installed"]) + var installed: Boolean = false, + + @JsonField(name = ["maintenance"]) + var maintenance: Boolean = false, + + @JsonField(name = ["upgrade"]) + var needsUpgrade: Boolean = false, + + @JsonField(name = ["version"]) + var version: String? = null, + + @JsonField(name = ["versionstring"]) + var versionString: String? = null, + + @JsonField(name = ["edition"]) + var edition: String? = null, + + @JsonField(name = ["productname"]) + var productName: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(false, false, false, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCard.kt b/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCard.kt new file mode 100644 index 0000000..62761b7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCard.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.hovercard + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class HoverCard( + @JsonField(name = ["userId"]) + var userId: String?, + @JsonField(name = ["displayName"]) + var displayName: String?, + @JsonField(name = ["actions"]) + var actions: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardAction.kt b/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardAction.kt new file mode 100644 index 0000000..5a9be55 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardAction.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.hovercard + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class HoverCardAction( + @JsonField(name = ["title"]) + var title: String?, + @JsonField(name = ["icon"]) + var icon: String?, + @JsonField(name = ["hyperlink"]) + var hyperlink: String?, + @JsonField(name = ["appId"]) + var appId: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOCS.kt new file mode 100644 index 0000000..2ca7cef --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021-2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.hovercard + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class HoverCardOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: HoverCard? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOverall.kt new file mode 100644 index 0000000..13827c3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/hovercard/HoverCardOverall.kt @@ -0,0 +1,19 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.hovercard + +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject + +@JsonObject +data class HoverCardOverall( + @JsonField(name = ["ocs"]) + var ocs: HoverCardOCS? +) { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/invitation/Invitation.kt b/app/src/main/java/com/nextcloud/talk/models/json/invitation/Invitation.kt new file mode 100644 index 0000000..9c8c095 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/invitation/Invitation.kt @@ -0,0 +1,43 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.invitation + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Invitation( + @JsonField(name = ["id"]) + var id: Int = 0, + @JsonField(name = ["state"]) + var state: Int = 0, + @JsonField(name = ["localCloudId"]) + var localCloudId: String? = null, + @JsonField(name = ["localToken"]) + var localToken: String? = null, + @JsonField(name = ["remoteAttendeeId"]) + var remoteAttendeeId: Int = 0, + @JsonField(name = ["remoteServerUrl"]) + var remoteServerUrl: String? = null, + @JsonField(name = ["remoteToken"]) + var remoteToken: String? = null, + @JsonField(name = ["roomName"]) + var roomName: String? = null, + @JsonField(name = ["userId"]) + var userId: String? = null, + @JsonField(name = ["inviterCloudId"]) + var inviterCloudId: String? = null, + @JsonField(name = ["inviterDisplayName"]) + var inviterDisplayName: String? = null + +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(0, 0, null, null, 0, null, null, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOCS.kt new file mode 100644 index 0000000..b4cef9a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOCS.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.invitation + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class InvitationOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOverall.kt new file mode 100644 index 0000000..1c8a269 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.invitation + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class InvitationOverall( + @JsonField(name = ["ocs"]) + var ocs: InvitationOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.kt b/app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.kt new file mode 100644 index 0000000..722562e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/mention/Mention.kt @@ -0,0 +1,39 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.mention + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonIgnore +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Mention( + @JsonField(name = ["mentionId"]) + var mentionId: String?, + @JsonField(name = ["id"]) + var id: String?, + @JsonField(name = ["label"]) + var label: String?, + // type of user (guests or users or calls) + @JsonField(name = ["source"]) + var source: String?, + @JsonField(name = ["status"]) + var status: String?, + @JsonField(name = ["statusIcon"]) + var statusIcon: String?, + @JsonField(name = ["statusMessage"]) + var statusMessage: String?, + @JsonIgnore + var roomToken: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/mention/MentionOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/mention/MentionOCS.kt new file mode 100644 index 0000000..f2815bf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/mention/MentionOCS.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.mention + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class MentionOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/mention/MentionOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/mention/MentionOverall.kt new file mode 100644 index 0000000..ed58512 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/mention/MentionOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.mention + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class MentionOverall( + @JsonField(name = ["ocs"]) + var ocs: MentionOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/notifications/Notification.kt b/app/src/main/java/com/nextcloud/talk/models/json/notifications/Notification.kt new file mode 100644 index 0000000..11822f9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/notifications/Notification.kt @@ -0,0 +1,53 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.notifications + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.converters.LoganSquareJodaTimeConverter +import kotlinx.parcelize.Parcelize +import org.joda.time.DateTime + +@Parcelize +@JsonObject +data class Notification( + @JsonField(name = ["icon"]) + var icon: String?, + @JsonField(name = ["notification_id"]) + var notificationId: Int?, + @JsonField(name = ["app"]) + var app: String?, + @JsonField(name = ["user"]) + var user: String?, + @JsonField(name = ["datetime"], typeConverter = LoganSquareJodaTimeConverter::class) + var datetime: DateTime?, + @JsonField(name = ["object_type"]) + var objectType: String?, + @JsonField(name = ["object_id"]) + var objectId: String?, + @JsonField(name = ["subject"]) + var subject: String?, + @JsonField(name = ["subjectRich"]) + var subjectRich: String?, + @JsonField(name = ["subjectRichParameters"]) + var subjectRichParameters: HashMap>?, + @JsonField(name = ["message"]) + var message: String?, + @JsonField(name = ["messageRich"]) + var messageRich: String?, + @JsonField(name = ["messageRichParameters"]) + var messageRichParameters: HashMap>?, + @JsonField(name = ["link"]) + var link: String?, + @JsonField(name = ["actions"]) + var actions: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationAction.kt b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationAction.kt new file mode 100644 index 0000000..816d878 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationAction.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.notifications + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class NotificationAction( + @JsonField(name = ["label"]) + var label: String?, + @JsonField(name = ["link"]) + var link: String?, + @JsonField(name = ["type"]) + var type: String?, + @JsonField(name = ["primary"]) + var primary: Boolean +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, false) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOCS.kt new file mode 100644 index 0000000..41546c1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.notifications + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class NotificationOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var notification: Notification? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOverall.kt new file mode 100644 index 0000000..49165c4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.notifications + +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject + +// see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md + +@JsonObject +data class NotificationOverall( + @JsonField(name = ["ocs"]) + var ocs: NotificationOCS? +) { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationRichObject.kt b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationRichObject.kt new file mode 100644 index 0000000..dbee921 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationRichObject.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.notifications + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class NotificationRichObject( + @JsonField(name = ["id"]) + var label: String?, + @JsonField(name = ["type"]) + var type: String?, + @JsonField(name = ["name"]) + var name: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationsOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationsOCS.kt new file mode 100644 index 0000000..350868c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationsOCS.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.notifications + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class NotificationsOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var notificationsList: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationsOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationsOverall.kt new file mode 100644 index 0000000..ca16167 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationsOverall.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.notifications + +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject + +@JsonObject +class NotificationsOverall( + @JsonField(name = ["ocs"]) + var ocs: NotificationsOCS? +) { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOCS.kt new file mode 100644 index 0000000..7ad6e9b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOCS.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.opengraph + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class OpenGraphOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: OpenGraphResponse? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphObject.kt b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphObject.kt new file mode 100644 index 0000000..42544aa --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphObject.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.opengraph + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class OpenGraphObject( + @JsonField(name = ["id"]) + var id: String, + @JsonField(name = ["name"]) + var name: String, + @JsonField(name = ["description"]) + var description: String? = null, + @JsonField(name = ["thumb"]) + var thumb: String? = null, + @JsonField(name = ["link"]) + var link: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this("", "", null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOverall.kt new file mode 100644 index 0000000..05fffdb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.opengraph + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class OpenGraphOverall( + @JsonField(name = ["ocs"]) + var ocs: OpenGraphOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphResponse.kt b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphResponse.kt new file mode 100644 index 0000000..3289902 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/OpenGraphResponse.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.opengraph + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class OpenGraphResponse( + @JsonField(name = ["references"]) + var references: HashMap? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/opengraph/Reference.kt b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/Reference.kt new file mode 100644 index 0000000..f79657c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/Reference.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.opengraph + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Reference( + @JsonField(name = ["richObjectType"]) + var richObjectType: String? = null, + @JsonField(name = ["richObject"]) + var richObject: RichObject? = null, + @JsonField(name = ["openGraphObject"]) + var openGraphObject: OpenGraphObject? = null, + @JsonField(name = ["accessible"]) + var accessible: Boolean +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, false) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/opengraph/RichObject.kt b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/RichObject.kt new file mode 100644 index 0000000..834a007 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/opengraph/RichObject.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.opengraph + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class RichObject( + @JsonField(name = ["id"]) + var id: String, + @JsonField(name = ["name"]) + var name: String, + @JsonField(name = ["description"]) + var description: String? = null, + @JsonField(name = ["thumb"]) + var thumb: String? = null, + @JsonField(name = ["link"]) + var link: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this("", "", null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/AddParticipantOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/AddParticipantOCS.kt new file mode 100644 index 0000000..034a8e5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/participants/AddParticipantOCS.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.participants + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class AddParticipantOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + /* Returned room will have only type set, and sometimes even that will be null */ + @JsonField(name = ["data"]) + var data: Conversation? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/AddParticipantOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/AddParticipantOverall.kt new file mode 100644 index 0000000..1bda191 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/participants/AddParticipantOverall.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.participants + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.conversations.RoomsOCS +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class AddParticipantOverall( + @JsonField(name = ["ocs"]) + var ocs: RoomsOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.kt new file mode 100644 index 0000000..0583c47 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/participants/Participant.kt @@ -0,0 +1,141 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.participants + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter +import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Participant( + @JsonField(name = ["attendeeId"]) + var attendeeId: Long? = null, + + @JsonField(name = ["actorType"], typeConverter = EnumActorTypeConverter::class) + var actorType: ActorType? = null, + + @JsonField(name = ["actorId"]) + var actorId: String? = null, + + @JsonField(name = ["attendeePin"]) + var attendeePin: String? = null, + + @Deprecated("") + @JsonField(name = ["userId"]) + var userId: String? = null, + + @JsonField(name = ["internal"]) + var internal: Boolean? = null, + + @JsonField(name = ["type", "participantType"], typeConverter = EnumParticipantTypeConverter::class) + var type: ParticipantType? = null, + + @Deprecated("") + @JsonField(name = ["name"]) + var name: String? = null, + + @JsonField(name = ["displayName"]) + var displayName: String? = null, + + @JsonField(name = ["lastPing"]) + var lastPing: Long = 0, + + @Deprecated("") + @JsonField(name = ["sessionId"]) + var sessionId: String? = null, + + @JsonField(name = ["sessionIds"]) + var sessionIds: ArrayList = ArrayList(0), + + @Deprecated("") + @JsonField(name = ["roomId"]) + var roomId: Long = 0, + + @JsonField(name = ["inCall"]) + var inCall: Long = 0, + + @JsonField(name = ["status"]) + var status: String? = null, + + @JsonField(name = ["statusIcon"]) + var statusIcon: String? = null, + + @JsonField(name = ["statusMessage"]) + var statusMessage: String? = null, + + @JsonField(name = ["invitedActorId"]) + var invitedActorId: String? = null, + + var selected: Boolean = false +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this( + null, null, null, null, null, null, null, null, null, + 0, null, ArrayList(0), 0, 0, null, + null, null + ) + + /** + * actorType is only guaranteed in APIv3+ so use calculatedActorType + * + * https://github.com/nextcloud/spreed/blob/stable21/lib/Controller/RoomController.php#L1145-L1148 + */ + val calculatedActorType: ActorType + get() = if (actorType == null) { + if (userId != null) { + ActorType.USERS + } else { + ActorType.GUESTS + } + } else { + actorType!! + } + + /** + * actorId is only guaranteed in APIv3+ so use calculatedActorId. + */ + val calculatedActorId: String? + get() = if (actorId == null) { + userId + } else { + actorId + } + + enum class ActorType { + DUMMY, + EMAILS, + GROUPS, + GUESTS, + USERS, + CIRCLES, + FEDERATED, + PHONES + } + + enum class ParticipantType { + DUMMY, + OWNER, + MODERATOR, + USER, + GUEST, + USER_FOLLOWING_LINK, + GUEST_MODERATOR + } + + object InCallFlags { + const val DISCONNECTED = 0 + const val IN_CALL = 1 + const val WITH_AUDIO = 2 + const val WITH_VIDEO = 4 + const val WITH_PHONE = 8 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/ParticipantsOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/ParticipantsOCS.kt new file mode 100644 index 0000000..4e135fc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/participants/ParticipantsOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.participants + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ParticipantsOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: List? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/ParticipantsOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/ParticipantsOverall.kt new file mode 100644 index 0000000..d82c90f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/participants/ParticipantsOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.participants + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ParticipantsOverall( + @JsonField(name = ["ocs"]) + var ocs: ParticipantsOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBan.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBan.kt new file mode 100644 index 0000000..63d60f4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBan.kt @@ -0,0 +1,40 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.participants + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TalkBan( + @JsonField(name = ["id"]) + var id: String?, + @JsonField(name = ["moderatorActorType"]) + var moderatorActorType: String?, + @JsonField(name = ["moderatorActorId"]) + var moderatorActorId: String?, + @JsonField(name = ["moderatorDisplayName"]) + var moderatorDisplayName: String?, + @JsonField(name = ["bannedActorType"]) + var bannedActorType: String?, + @JsonField(name = ["bannedActorId"]) + var bannedActorId: String?, + @JsonField(name = ["bannedDisplayName"]) + var bannedDisplayName: String?, + @JsonField(name = ["bannedTime"]) + var bannedTime: Int?, + @JsonField(name = ["internalNote"]) + var internalNote: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : + this(null, null, null, null, null, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOCS.kt new file mode 100644 index 0000000..9dd62a2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.participants + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TalkBanOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: List? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOverall.kt new file mode 100644 index 0000000..08174ec --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/participants/TalkBanOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.participants + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TalkBanOverall( + @JsonField(name = ["ocs"]) + var ocs: TalkBanOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/profile/CoreProfileAction.kt b/app/src/main/java/com/nextcloud/talk/models/json/profile/CoreProfileAction.kt new file mode 100644 index 0000000..365f324 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/profile/CoreProfileAction.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.profile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class CoreProfileAction( + @JsonField(name = ["id"]) + var id: String? = null, + @JsonField(name = ["icon"]) + var icon: String? = null, + @JsonField(name = ["title"]) + var title: String? = null, + @JsonField(name = ["target"]) + var target: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/profile/Profile.kt b/app/src/main/java/com/nextcloud/talk/models/json/profile/Profile.kt new file mode 100644 index 0000000..a23388b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/profile/Profile.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.profile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Profile( + @JsonField(name = ["userId"]) var userId: String? = null, + @JsonField(name = ["address"]) var address: String? = null, + @JsonField(name = ["biography"]) var biography: Int? = null, + @JsonField(name = ["displayname"]) var displayName: Int? = null, + @JsonField(name = ["headline"]) var headline: String? = null, + // @JsonField(name = ["isUserAvatarVisible"]) var isUserAvatarVisible: Boolean = false, + @JsonField(name = ["organisation"]) var company: String? = null, + @JsonField(name = ["pronouns"]) var pronouns: String? = null, + @JsonField(name = ["role"]) var role: String? = null, + @JsonField(name = ["actions"]) var actions: List? = null, + @JsonField(name = ["timezone"]) var timezone: String? = null, + @JsonField(name = ["timezoneOffset"]) var timezoneOffset: Int? = null +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOCS.kt new file mode 100644 index 0000000..b26391e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.profile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ProfileOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta? = null, + @JsonField(name = ["data"]) + var data: Profile? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOverall.kt new file mode 100644 index 0000000..7ba466f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/profile/ProfileOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.profile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ProfileOverall( + @JsonField(name = ["ocs"]) + var ocs: ProfileOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt new file mode 100644 index 0000000..e3bb3cc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt @@ -0,0 +1,106 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.push + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonIgnore +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class DecryptedPushMessage( + @JsonField(name = ["app"]) + var app: String?, + + @JsonField(name = ["type"]) + var type: String?, + + @JsonField(name = ["subject"]) + var subject: String, + + @JsonField(name = ["id"]) + var id: String?, + + @JsonField(name = ["nid"]) + var notificationId: Long?, + + @JsonField(name = ["nids"]) + var notificationIds: LongArray?, + + @JsonField(name = ["delete"]) + var delete: Boolean, + + @JsonField(name = ["delete-all"]) + var deleteAll: Boolean, + + @JsonField(name = ["delete-multiple"]) + var deleteMultiple: Boolean, + + @JsonIgnore + var notificationUser: NotificationUser?, + + @JsonIgnore + var text: String?, + + @JsonIgnore + var timestamp: Long, + + @JsonIgnore + var objectId: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, "", null, 0, null, false, false, false, null, null, 0, null) + + @Suppress("Detekt.ComplexMethod") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DecryptedPushMessage + + if (app != other.app) return false + if (type != other.type) return false + if (subject != other.subject) return false + if (id != other.id) return false + if (notificationId != other.notificationId) return false + if (notificationIds != null) { + if (other.notificationIds == null) return false + if (!notificationIds.contentEquals(other.notificationIds)) return false + } else if (other.notificationIds != null) { + return false + } + if (delete != other.delete) return false + if (deleteAll != other.deleteAll) return false + if (deleteMultiple != other.deleteMultiple) return false + if (notificationUser != other.notificationUser) return false + if (text != other.text) return false + if (timestamp != other.timestamp) return false + if (objectId != other.objectId) return false + + return true + } + + override fun hashCode(): Int { + var result = app?.hashCode() ?: 0 + result = 31 * result + (type?.hashCode() ?: 0) + result = 31 * result + (subject?.hashCode() ?: 0) + result = 31 * result + (id?.hashCode() ?: 0) + result = 31 * result + (notificationId?.hashCode() ?: 0) + result = 31 * result + (notificationIds?.contentHashCode() ?: 0) + result = 31 * result + (delete?.hashCode() ?: 0) + result = 31 * result + (deleteAll?.hashCode() ?: 0) + result = 31 * result + (deleteMultiple?.hashCode() ?: 0) + result = 31 * result + (notificationUser?.hashCode() ?: 0) + result = 31 * result + (text?.hashCode() ?: 0) + result = 31 * result + (timestamp?.hashCode() ?: 0) + result = 31 * result + (objectId?.hashCode() ?: 0) + return result + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/NotificationUser.kt b/app/src/main/java/com/nextcloud/talk/models/json/push/NotificationUser.kt new file mode 100644 index 0000000..594bb03 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/NotificationUser.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.push + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class NotificationUser( + @JsonField(name = ["type"]) + var type: String?, + + @JsonField(name = ["id"]) + var id: String?, + + @JsonField(name = ["name"]) + var name: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/PushConfigurationState.kt b/app/src/main/java/com/nextcloud/talk/models/json/push/PushConfigurationState.kt new file mode 100644 index 0000000..ad3102a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/PushConfigurationState.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.push + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PushConfigurationState( + @JsonField(name = ["pushToken"]) + var pushToken: String?, + + @JsonField(name = ["deviceIdentifier"]) + var deviceIdentifier: String?, + + @JsonField(name = ["deviceIdentifierSignature"]) + var deviceIdentifierSignature: String?, + + @JsonField(name = ["userPublicKey"]) + var userPublicKey: String?, + + @JsonField(name = ["usesRegularPass"]) + var usesRegularPass: Boolean? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null, false) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistration.kt b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistration.kt new file mode 100644 index 0000000..f71c735 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistration.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.push + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PushRegistration( + @JsonField(name = ["publicKey"]) + var publicKey: String?, + + @JsonField(name = ["deviceIdentifier"]) + var deviceIdentifier: String?, + + @JsonField(name = ["signature"]) + var signature: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOCS.kt new file mode 100644 index 0000000..cb92b86 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.push + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PushRegistrationOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: PushRegistration? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOverall.kt new file mode 100644 index 0000000..dbc46ab --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.push + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PushRegistrationOverall( + @JsonField(name = ["ocs"]) + var ocs: PushRegistrationOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionVoter.kt b/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionVoter.kt new file mode 100644 index 0000000..f9f7e12 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionVoter.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.reactions + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.converters.EnumReactionActorTypeConverter +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ReactionVoter( + @JsonField(name = ["actorType"], typeConverter = EnumReactionActorTypeConverter::class) + var actorType: ReactionActorType?, + @JsonField(name = ["actorId"]) + var actorId: String?, + @JsonField(name = ["actorDisplayName"]) + var actorDisplayName: String?, + @JsonField(name = ["timestamp"]) + var timestamp: Long = 0 +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, 0) + + enum class ReactionActorType { + DUMMY, + GUESTS, + USERS + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOCS.kt new file mode 100644 index 0000000..52ef976 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOCS.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.reactions + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize +import java.util.HashMap + +@Parcelize +@JsonObject +data class ReactionsOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: HashMap>? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, HashMap()) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOverall.kt new file mode 100644 index 0000000..e198bc7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.reactions + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ReactionsOverall( + @JsonField(name = ["ocs"]) + var ocs: ReactionsOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/reminder/Reminder.kt b/app/src/main/java/com/nextcloud/talk/models/json/reminder/Reminder.kt new file mode 100644 index 0000000..9f46c15 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/reminder/Reminder.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.reminder + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Reminder( + @JsonField(name = ["userid"]) + var userid: String? = null, + @JsonField(name = ["token"]) + var token: String? = null, + @JsonField(name = ["messageId"]) + var messageId: Int? = null, + @JsonField(name = ["timestamp"]) + var timestamp: Int? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/reminder/ReminderOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/reminder/ReminderOCS.kt new file mode 100644 index 0000000..bfbb1a8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/reminder/ReminderOCS.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.reminder + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ReminderOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta? = null, + @JsonField(name = ["data"]) + var data: Reminder? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/reminder/ReminderOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/reminder/ReminderOverall.kt new file mode 100644 index 0000000..49af4a1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/reminder/ReminderOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.reminder + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ReminderOverall( + @JsonField(name = ["ocs"]) + var ocs: ReminderOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOCS.kt new file mode 100644 index 0000000..3ba9306 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.search + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ContactsByNumberOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var map: Map? = HashMap() +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, HashMap()) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOverall.kt new file mode 100644 index 0000000..329618c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/search/ContactsByNumberOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.search + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ContactsByNumberOverall( + @JsonField(name = ["ocs"]) + var ocs: ContactsByNumberOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/sharees/ExactSharees.kt b/app/src/main/java/com/nextcloud/talk/models/json/sharees/ExactSharees.kt new file mode 100644 index 0000000..82d91de --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/sharees/ExactSharees.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.sharees + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ExactSharees( + @JsonField(name = ["users"]) + var exactSharees: List? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/sharees/Sharee.kt b/app/src/main/java/com/nextcloud/talk/models/json/sharees/Sharee.kt new file mode 100644 index 0000000..eab9093 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/sharees/Sharee.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.sharees + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Sharee( + @JsonField(name = ["id"]) + var id: String? = null, + @JsonField(name = ["value"]) + var value: Value? = null, + @JsonField(name = ["label"]) + var label: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/sharees/ShareesOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/sharees/ShareesOCS.kt new file mode 100644 index 0000000..f64ed2e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/sharees/ShareesOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.sharees + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ShareesOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: SharesData? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/sharees/ShareesOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/sharees/ShareesOverall.kt new file mode 100644 index 0000000..7be2fc7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/sharees/ShareesOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.sharees + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ShareesOverall( + @JsonField(name = ["ocs"]) + var ocs: ShareesOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/sharees/SharesData.kt b/app/src/main/java/com/nextcloud/talk/models/json/sharees/SharesData.kt new file mode 100644 index 0000000..cab6de4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/sharees/SharesData.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.sharees + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class SharesData( + @JsonField(name = ["users"]) + var users: List? = null, + @JsonField(name = ["exact"]) + var exactUsers: ExactSharees? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/sharees/Value.kt b/app/src/main/java/com/nextcloud/talk/models/json/sharees/Value.kt new file mode 100644 index 0000000..afacb61 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/sharees/Value.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.sharees + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Value( + @JsonField(name = ["shareWith"]) + var shareWith: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/DataChannelMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/DataChannelMessage.kt new file mode 100644 index 0000000..a350059 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/DataChannelMessage.kt @@ -0,0 +1,34 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.AnyParceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler + +@Parcelize +@JsonObject +@TypeParceler +data class DataChannelMessage( + @JsonField(name = ["type"]) + var type: String? = null, + /** Can be String or Map + * Use only for received messages */ + @JsonField(name = ["payload"]) + var payload: Any? = null, + /** Use only to send messages */ + @JsonField(name = ["payload"]) + var payloadMap: Map? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) + constructor(type: String) : this(type, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCIceCandidate.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCIceCandidate.kt new file mode 100644 index 0000000..c4336c8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCIceCandidate.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class NCIceCandidate( + @JsonField(name = ["sdpMLineIndex"]) + var sdpMLineIndex: Int = 0, + @JsonField(name = ["sdpMid"]) + var sdpMid: String? = null, + @JsonField(name = ["candidate"]) + var candidate: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(0, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessagePayload.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessagePayload.kt new file mode 100644 index 0000000..3f0151e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessagePayload.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class NCMessagePayload( + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["sdp"]) + var sdp: String? = null, + @JsonField(name = ["nick"]) + var nick: String? = null, + @JsonField(name = ["candidate"]) + var iceCandidate: NCIceCandidate? = null, + @JsonField(name = ["name"]) + var name: String? = null, + @JsonField(name = ["state"]) + var state: Boolean? = null, + @JsonField(name = ["timestamp"]) + var timestamp: Long? = null, + @JsonField(name = ["reaction"]) + var reaction: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCSignalingMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCSignalingMessage.kt new file mode 100644 index 0000000..0a63a1f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCSignalingMessage.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class NCSignalingMessage( + @JsonField(name = ["from"]) + var from: String? = null, + @JsonField(name = ["to"]) + var to: String? = null, + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["payload"]) + var payload: NCMessagePayload? = null, + @JsonField(name = ["roomType"]) + var roomType: String? = null, + @JsonField(name = ["sid"]) + var sid: String? = null, + @JsonField(name = ["prefix"]) + var prefix: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/Signaling.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/Signaling.kt new file mode 100644 index 0000000..4fb2bb5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/Signaling.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.AnyParceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler + +@Parcelize +@JsonObject +@TypeParceler +data class Signaling( + @JsonField(name = ["type"]) + var type: String? = null, + /** can be NCSignalingMessage (encoded as a String) or List> */ + @JsonField(name = ["data"]) + var messageWrapper: Any? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOCS.kt new file mode 100644 index 0000000..892b48f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class SignalingOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var signalings: List? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOverall.kt new file mode 100644 index 0000000..2a1dbe4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class SignalingOverall( + @JsonField(name = ["ocs"]) + var ocs: SignalingOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationHelloAuthParams.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationHelloAuthParams.kt new file mode 100644 index 0000000..290a8f8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationHelloAuthParams.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling.settings + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class FederationHelloAuthParams( + @JsonField(name = ["token"]) + var token: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationSettings.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationSettings.kt new file mode 100644 index 0000000..27c2936 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationSettings.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling.settings + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class FederationSettings( + @JsonField(name = ["server"]) + var server: String? = null, + @JsonField(name = ["nextcloudServer"]) + var nextcloudServer: String? = null, + @JsonField(name = ["helloAuthParams"]) + var helloAuthParams: FederationHelloAuthParams? = null, + @JsonField(name = ["roomId"]) + var roomId: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.kt new file mode 100644 index 0000000..54b0bed --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling.settings + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class IceServer( + @Deprecated("") + @JsonField(name = ["url"]) + var url: String? = null, + @JsonField(name = ["urls"]) + var urls: List? = null, + @JsonField(name = ["username"]) + var username: String? = null, + @JsonField(name = ["credential"]) + var credential: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettings.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettings.kt new file mode 100644 index 0000000..89dc837 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettings.kt @@ -0,0 +1,33 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling.settings + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Parcelize +@JsonObject +@Serializable +data class SignalingSettings( + @JsonField(name = ["stunservers"]) + var stunServers: List? = null, + @JsonField(name = ["turnservers"]) + var turnServers: List? = null, + @JsonField(name = ["server"]) + var externalSignalingServer: String? = null, + @JsonField(name = ["ticket"]) + var externalSignalingTicket: String? = null, + @JsonField(name = ["federation"]) + var federation: FederationSettings? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.kt new file mode 100644 index 0000000..d6ec978 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling.settings + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class SignalingSettingsOcs( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var settings: SignalingSettings? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOverall.kt new file mode 100644 index 0000000..0333828 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.signaling.settings + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class SignalingSettingsOverall( + @JsonField(name = ["ocs"]) + var ocs: SignalingSettingsOcs? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/ClearAt.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/ClearAt.kt new file mode 100644 index 0000000..1272432 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/status/ClearAt.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.status + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ClearAt( + @JsonField(name = ["type"]) + var type: String, + @JsonField(name = ["time"]) + var time: String +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this("type", "time") +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/Status.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/Status.kt new file mode 100644 index 0000000..8aae7b1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/status/Status.kt @@ -0,0 +1,39 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.status + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Status( + @JsonField(name = ["userId"]) + var userId: String?, + @JsonField(name = ["message"]) + var message: String?, + /* TODO Change to enum */ + @JsonField(name = ["messageId"]) + var messageId: String?, + @JsonField(name = ["messageIsPredefined"]) + var messageIsPredefined: Boolean, + @JsonField(name = ["icon"]) + var icon: String?, + @JsonField(name = ["clearAt"]) + var clearAt: Long = 0, + /* TODO Change to enum */ + @JsonField(name = ["status"]) + var status: String = "offline", + @JsonField(name = ["statusIsUserDefined"]) + var statusIsUserDefined: Boolean +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, false, null, 0, "offline", false) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/StatusOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/StatusOCS.kt new file mode 100644 index 0000000..6d7e986 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/status/StatusOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.status + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class StatusOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: Status? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/StatusOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/StatusOverall.kt new file mode 100644 index 0000000..06ea362 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/status/StatusOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.status + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class StatusOverall( + @JsonField(name = ["ocs"]) + var ocs: StatusOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/StatusType.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/StatusType.kt new file mode 100644 index 0000000..549f835 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/status/StatusType.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.status + +enum class StatusType(val string: String) { + ONLINE("online"), + OFFLINE("offline"), + DND("dnd"), + AWAY("away"), + BUSY("busy"), + INVISIBLE("invisible") +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatus.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatus.kt new file mode 100644 index 0000000..19dd1b0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatus.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.status.predefined + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.status.ClearAt +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PredefinedStatus( + @JsonField(name = ["id"]) + var id: String, + @JsonField(name = ["icon"]) + var icon: String?, + @JsonField(name = ["message"]) + var message: String, + @JsonField(name = ["clearAt"]) + var clearAt: ClearAt? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this("id", "icon", "message", null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOCS.kt new file mode 100644 index 0000000..58c2575 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.status.predefined + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PredefinedStatusOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOverall.kt new file mode 100644 index 0000000..da546a3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/status/predefined/PredefinedStatusOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.status.predefined + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PredefinedStatusOverall( + @JsonField(name = ["ocs"]) + var ocs: PredefinedStatusOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationData.kt b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationData.kt new file mode 100644 index 0000000..451b9a2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationData.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.testNotification + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TestNotificationData( + @JsonField(name = ["message"]) + var message: String +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : + this("") +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOCS.kt new file mode 100644 index 0000000..b43fe1f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.testNotification + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TestNotificationOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: TestNotificationData? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOverall.kt new file mode 100644 index 0000000..140d4b9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/testNotification/TestNotificationOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.testNotification + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TestNotificationOverall( + @JsonField(name = ["ocs"]) + var ocs: TestNotificationOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/Thread.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/Thread.kt new file mode 100644 index 0000000..a66a568 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/Thread.kt @@ -0,0 +1,34 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Thread( + @JsonField(name = ["id"]) + var id: Int = 0, + + @JsonField(name = ["roomToken"]) + var roomToken: String = "", + + @JsonField(name = ["title"]) + var title: String = "", + + @JsonField(name = ["lastMessageId"]) + var lastMessageId: Int = 0, + + @JsonField(name = ["lastActivity"]) + var lastActivity: Int = 0, + + @JsonField(name = ["numReplies"]) + var numReplies: Int = 0 +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadAttendee.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadAttendee.kt new file mode 100644 index 0000000..120d4e8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadAttendee.kt @@ -0,0 +1,19 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ThreadAttendee( + @JsonField(name = ["notificationLevel"]) + var notificationLevel: Int = 0 +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadInfo.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadInfo.kt new file mode 100644 index 0000000..50ea57d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadInfo.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ThreadInfo( + @JsonField(name = ["thread"]) + var thread: Thread? = null, + + @JsonField(name = ["attendee"]) + var attendee: ThreadAttendee? = null, + + @JsonField(name = ["first"]) + var first: ChatMessageJson? = null, + + @JsonField(name = ["last"]) + var last: ChatMessageJson? = null +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOCS.kt new file mode 100644 index 0000000..56b2d65 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOCS.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ThreadOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: ThreadInfo? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOverall.kt new file mode 100644 index 0000000..c622fcf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ThreadOverall( + @JsonField(name = ["ocs"]) + var ocs: ThreadOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadsOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadsOCS.kt new file mode 100644 index 0000000..bafb22f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadsOCS.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ThreadsOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: List? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadsOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadsOverall.kt new file mode 100644 index 0000000..99fdab5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/threads/ThreadsOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.threads + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ThreadsOverall( + @JsonField(name = ["ocs"]) + var ocs: ThreadsOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchEntry.kt b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchEntry.kt new file mode 100644 index 0000000..d142949 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchEntry.kt @@ -0,0 +1,34 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.unifiedsearch + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UnifiedSearchEntry( + @JsonField(name = ["thumbnailUrl"]) + var thumbnailUrl: String?, + @JsonField(name = ["title"]) + var title: String?, + @JsonField(name = ["subline"]) + var subline: String?, + @JsonField(name = ["resourceUrl"]) + var resourceUrl: String?, + @JsonField(name = ["icon"]) + var icon: String?, + @JsonField(name = ["rounded"]) + var rounded: Boolean?, + @JsonField(name = ["attributes"]) + var attributes: Map? +) : Parcelable { + constructor() : this(null, null, null, null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOCS.kt new file mode 100644 index 0000000..25ba032 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOCS.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.unifiedsearch + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UnifiedSearchOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: UnifiedSearchResponseData? +) : Parcelable { + // Empty constructor needed for JsonObject + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOverall.kt new file mode 100644 index 0000000..4c68b12 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.unifiedsearch + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UnifiedSearchOverall( + @JsonField(name = ["ocs"]) + var ocs: UnifiedSearchOCS? +) : Parcelable { + // Empty constructor needed for JsonObject + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchResponseData.kt b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchResponseData.kt new file mode 100644 index 0000000..dd11074 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchResponseData.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.unifiedsearch + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UnifiedSearchResponseData( + @JsonField(name = ["name"]) + var name: String?, + @JsonField(name = ["isPaginated"]) + var paginated: Boolean?, + @JsonField(name = ["entries"]) + var entries: List?, + @JsonField(name = ["cursor"]) + var cursor: Int? +) : Parcelable { + // empty constructor needed for JsonObject + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceData.kt b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceData.kt new file mode 100644 index 0000000..77e71e8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceData.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.userAbsence + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UserAbsenceData( + @JsonField(name = ["id"]) + var id: String, + @JsonField(name = ["userId"]) + var userId: String, + @JsonField(name = ["startDate"]) + var startDate: Int, + @JsonField(name = ["endDate"]) + var endDate: Int, + @JsonField(name = ["shortMessage"]) + var shortMessage: String, + @JsonField(name = ["message"]) + var message: String, + @JsonField(name = ["replacementUserId"]) + var replacementUserId: String?, + @JsonField(name = ["replacementUserDisplayName"]) + var replacementUserDisplayName: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : + this("", "", 0, 0, "", "", null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOCS.kt new file mode 100644 index 0000000..06a80c4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.userAbsence + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UserAbsenceOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: UserAbsenceData? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOverall.kt new file mode 100644 index 0000000..7f6fada --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userAbsence/UserAbsenceOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.models.json.userAbsence + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UserAbsenceOverall( + @JsonField(name = ["ocs"]) + var ocs: UserAbsenceOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/Scope.kt b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/Scope.kt new file mode 100644 index 0000000..8a7c9b4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/Scope.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.userprofile + +enum class Scope(val id: String) { + PRIVATE("v2-private"), + LOCAL("v2-local"), + FEDERATED("v2-federated"), + PUBLISHED("v2-published") +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileData.kt b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileData.kt new file mode 100644 index 0000000..8feaebd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileData.kt @@ -0,0 +1,86 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.userprofile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.converters.ScopeConverter +import com.nextcloud.talk.profile.ProfileActivity +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UserProfileData( + @JsonField(name = ["display-name"]) + var displayName: String?, + + @JsonField(name = ["displaynameScope"], typeConverter = ScopeConverter::class) + var displayNameScope: Scope?, + + @JsonField(name = ["displayname"]) + var displayNameAlt: String?, + + @JsonField(name = ["id"]) + var userId: String?, + + @JsonField(name = ["phone"]) + var phone: String?, + + @JsonField(name = ["phoneScope"], typeConverter = ScopeConverter::class) + var phoneScope: Scope?, + + @JsonField(name = ["email"]) + var email: String?, + + @JsonField(name = ["emailScope"], typeConverter = ScopeConverter::class) + var emailScope: Scope?, + + @JsonField(name = ["address"]) + var address: String?, + + @JsonField(name = ["addressScope"], typeConverter = ScopeConverter::class) + var addressScope: Scope?, + + @JsonField(name = ["twitter"]) + var twitter: String?, + + @JsonField(name = ["twitterScope"], typeConverter = ScopeConverter::class) + var twitterScope: Scope?, + + @JsonField(name = ["website"]) + var website: String?, + + @JsonField(name = ["websiteScope"], typeConverter = ScopeConverter::class) + var websiteScope: Scope? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null, null, null, null, null, null, null, null, null, null, null) + + fun getValueByField(field: ProfileActivity.Field?): String? = + when (field) { + ProfileActivity.Field.EMAIL -> email + ProfileActivity.Field.DISPLAYNAME -> displayName + ProfileActivity.Field.PHONE -> phone + ProfileActivity.Field.ADDRESS -> address + ProfileActivity.Field.WEBSITE -> website + ProfileActivity.Field.TWITTER -> twitter + else -> "" + } + + fun getScopeByField(field: ProfileActivity.Field?): Scope? = + when (field) { + ProfileActivity.Field.EMAIL -> emailScope + ProfileActivity.Field.DISPLAYNAME -> displayNameScope + ProfileActivity.Field.PHONE -> phoneScope + ProfileActivity.Field.ADDRESS -> addressScope + ProfileActivity.Field.WEBSITE -> websiteScope + ProfileActivity.Field.TWITTER -> twitterScope + else -> null + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.kt new file mode 100644 index 0000000..02d61d2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOCS.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.userprofile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize +import java.util.ArrayList + +@Parcelize +@JsonObject +data class UserProfileFieldsOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: ArrayList? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOverall.kt new file mode 100644 index 0000000..bd4be63 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileFieldsOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.userprofile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UserProfileFieldsOverall( + @JsonField(name = ["ocs"]) + var ocs: UserProfileFieldsOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileOCS.kt new file mode 100644 index 0000000..9e4f419 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileOCS.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.userprofile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UserProfileOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: UserProfileData? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileOverall.kt new file mode 100644 index 0000000..14f928a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/userprofile/UserProfileOverall.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.userprofile + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class UserProfileOverall( + @JsonField(name = ["ocs"]) + var ocs: UserProfileOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/ActorWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ActorWebSocketMessage.kt new file mode 100644 index 0000000..f6c7bb8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ActorWebSocketMessage.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ActorWebSocketMessage( + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["sessionid"]) + var sessionId: String? = null, + @JsonField(name = ["userid"]) + var userid: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/AuthParametersWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/AuthParametersWebSocketMessage.kt new file mode 100644 index 0000000..9040248 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/AuthParametersWebSocketMessage.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class AuthParametersWebSocketMessage( + @JsonField(name = ["userid"]) + var userid: String? = null, + @JsonField(name = ["ticket"]) + var ticket: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/AuthWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/AuthWebSocketMessage.kt new file mode 100644 index 0000000..9703a19 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/AuthWebSocketMessage.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class AuthWebSocketMessage( + @JsonField(name = ["url"]) + var url: String? = null, + @JsonField(name = ["params"]) + var authParametersWebSocketMessage: AuthParametersWebSocketMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessage.kt new file mode 100644 index 0000000..1208e8e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessage.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class BaseWebSocketMessage( + @JsonField(name = ["type"]) + var type: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/ByeWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ByeWebSocketMessage.kt new file mode 100644 index 0000000..a33ecab --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ByeWebSocketMessage.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.AnyParceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler +import java.util.HashMap + +@Parcelize +@JsonObject +@TypeParceler +data class ByeWebSocketMessage( + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["bye"]) + var bye: HashMap? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/CallOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/CallOverallWebSocketMessage.kt new file mode 100644 index 0000000..5e13a1a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/CallOverallWebSocketMessage.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class CallOverallWebSocketMessage( + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["message"]) + var callWebSocketMessage: CallWebSocketMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/CallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/CallWebSocketMessage.kt new file mode 100644 index 0000000..2915b14 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/CallWebSocketMessage.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class CallWebSocketMessage( + @JsonField(name = ["recipient"]) + var recipientWebSocketMessage: ActorWebSocketMessage? = null, + @JsonField(name = ["sender"]) + var senderWebSocketMessage: ActorWebSocketMessage? = null, + @JsonField(name = ["data"]) + var ncSignalingMessage: NCSignalingMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorOverallWebSocketMessage.kt new file mode 100644 index 0000000..90ceafd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorOverallWebSocketMessage.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ErrorOverallWebSocketMessage( + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["error"]) + var errorWebSocketMessage: ErrorWebSocketMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorWebSocketMessage.kt new file mode 100644 index 0000000..14d7e4f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorWebSocketMessage.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ErrorWebSocketMessage( + @JsonField(name = ["code"]) + var code: String? = null, + @JsonField(name = ["message"]) + var message: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/EventOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/EventOverallWebSocketMessage.kt new file mode 100644 index 0000000..969cf2b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/EventOverallWebSocketMessage.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.AnyParceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler +import java.util.HashMap + +@Parcelize +@JsonObject +@TypeParceler +class EventOverallWebSocketMessage( + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["event"]) + var eventMap: HashMap? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloOverallWebSocketMessage.kt new file mode 100644 index 0000000..8d213df --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloOverallWebSocketMessage.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class HelloOverallWebSocketMessage( + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["hello"]) + var helloWebSocketMessage: HelloWebSocketMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseOverallWebSocketMessage.kt new file mode 100644 index 0000000..025bddf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseOverallWebSocketMessage.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class HelloResponseOverallWebSocketMessage( + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["hello"]) + var helloResponseWebSocketMessage: HelloResponseWebSocketMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseWebSocketMessage.kt new file mode 100644 index 0000000..10f6c6d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseWebSocketMessage.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class HelloResponseWebSocketMessage( + @JsonField(name = ["resumeid"]) + var resumeId: String? = null, + @JsonField(name = ["sessionid"]) + var sessionId: String? = null, + @JsonField(name = ["server"]) + var serverHelloResponseFeaturesWebSocketMessage: ServerHelloResponseFeaturesWebSocketMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) + + fun serverHasMCUSupport(): Boolean = + serverHelloResponseFeaturesWebSocketMessage != null && + serverHelloResponseFeaturesWebSocketMessage!!.features != null && + serverHelloResponseFeaturesWebSocketMessage!!.features!!.contains("mcu") +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloWebSocketMessage.kt new file mode 100644 index 0000000..0ab3320 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloWebSocketMessage.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class HelloWebSocketMessage( + @JsonField(name = ["version"]) + var version: String? = null, + @JsonField(name = ["resumeid"]) + var resumeid: String? = null, + @JsonField(name = ["auth"]) + var authWebSocketMessage: AuthWebSocketMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/JoinedRoomOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/JoinedRoomOverallWebSocketMessage.kt new file mode 100644 index 0000000..780367b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/JoinedRoomOverallWebSocketMessage.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class JoinedRoomOverallWebSocketMessage( + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["room"]) + var roomWebSocketMessage: RoomWebSocketMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomFederationWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomFederationWebSocketMessage.kt new file mode 100644 index 0000000..e5352bd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomFederationWebSocketMessage.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class RoomFederationWebSocketMessage( + @JsonField(name = ["signaling"]) + var signaling: String? = null, + @JsonField(name = ["url"]) + var url: String? = null, + @JsonField(name = ["roomid"]) + var roomid: String? = null, + @JsonField(name = ["token"]) + var token: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomOverallWebSocketMessage.kt new file mode 100644 index 0000000..a347d18 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomOverallWebSocketMessage.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class RoomOverallWebSocketMessage( + @JsonField(name = ["type"]) + var type: String? = null, + @JsonField(name = ["room"]) + var roomWebSocketMessage: RoomWebSocketMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomPropertiesWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomPropertiesWebSocketMessage.kt new file mode 100644 index 0000000..d2cca28 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomPropertiesWebSocketMessage.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.converters.EnumRoomTypeConverter +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class RoomPropertiesWebSocketMessage( + @JsonField(name = ["name"]) + var name: String? = null, + @JsonField(name = ["type"], typeConverter = EnumRoomTypeConverter::class) + var roomType: ConversationEnums.ConversationType? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomWebSocketMessage.kt new file mode 100644 index 0000000..ce3af7f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomWebSocketMessage.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class RoomWebSocketMessage( + @JsonField(name = ["roomid"]) + var roomId: String? = null, + @JsonField(name = ["sessionid"]) + var sessionId: String? = null, + @JsonField(name = ["properties"]) + var roomPropertiesWebSocketMessage: RoomPropertiesWebSocketMessage? = null, + @JsonField(name = ["federation"]) + var roomFederationWebSocketMessage: RoomFederationWebSocketMessage? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/ServerHelloResponseFeaturesWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ServerHelloResponseFeaturesWebSocketMessage.kt new file mode 100644 index 0000000..a8cf8df --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ServerHelloResponseFeaturesWebSocketMessage.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class ServerHelloResponseFeaturesWebSocketMessage( + @JsonField(name = ["features"]) + var features: List? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt new file mode 100644 index 0000000..9ceea14 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt @@ -0,0 +1,152 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.openconversations + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.core.graphics.drawable.toDrawable +import androidx.core.widget.doOnTextChanged +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.databinding.ActivityOpenConversationsBinding +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.openconversations.adapters.OpenConversationsAdapter +import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.vanniktech.ui.hideKeyboardAndFocus +import com.vanniktech.ui.showKeyboardAndFocus +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ListOpenConversationsActivity : BaseActivity() { + + private lateinit var binding: ActivityOpenConversationsBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + lateinit var openConversationsViewModel: OpenConversationsViewModel + + lateinit var adapter: OpenConversationsAdapter + + var searching = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + openConversationsViewModel = ViewModelProvider(this, viewModelFactory)[OpenConversationsViewModel::class.java] + + openConversationsViewModel.fetchConversations() + + binding = ActivityOpenConversationsBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + initSystemBars() + viewThemeUtils.platform.colorImageView(binding.searchOpenConversations, ColorRole.ON_SURFACE) + viewThemeUtils.material.colorTextInputLayout(binding.textInputLayout) + + val user = currentUserProvider.currentUser.blockingGet() + + adapter = OpenConversationsAdapter(user, viewThemeUtils) { conversation -> adapterOnClick(conversation) } + binding.openConversationsRecyclerView.adapter = adapter + binding.searchOpenConversations.setOnClickListener { + searching = !searching + handleSearchUI(searching) + } + binding.editText.doOnTextChanged { text, _, _, count -> + if (!text.isNullOrBlank()) { + openConversationsViewModel.updateSearchTerm(text.toString()) + openConversationsViewModel.fetchConversations() + } else { + openConversationsViewModel.updateSearchTerm("") + openConversationsViewModel.fetchConversations() + } + } + initObservers() + } + + private fun handleSearchUI(show: Boolean) { + if (show) { + binding.searchOpenConversations.visibility = View.GONE + binding.textInputLayout.visibility = View.VISIBLE + binding.editText.showKeyboardAndFocus() + } else { + binding.searchOpenConversations.visibility = View.VISIBLE + binding.textInputLayout.visibility = View.GONE + binding.editText.hideKeyboardAndFocus() + } + } + + private fun adapterOnClick(conversation: Conversation) { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation.token) + + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(chatIntent) + } + + private fun initObservers() { + openConversationsViewModel.viewState.observe(this) { state -> + when (state) { + is OpenConversationsViewModel.FetchConversationsStartState -> { + binding.openConversationsRecyclerView.visibility = View.GONE + binding.progressBarWrapper.visibility = View.VISIBLE + } + is OpenConversationsViewModel.FetchConversationsSuccessState -> { + binding.openConversationsRecyclerView.visibility = View.VISIBLE + binding.progressBarWrapper.visibility = View.GONE + adapter.submitList(state.conversations) + } + is OpenConversationsViewModel.FetchConversationsEmptyState -> { + binding.openConversationsRecyclerView.visibility = View.GONE + binding.progressBarWrapper.visibility = View.GONE + + binding.emptyList.emptyListView.visibility = View.VISIBLE + binding.emptyList.emptyListViewHeadline.text = getString(R.string.nc_no_open_conversations_headline) + binding.emptyList.emptyListViewText.text = getString(R.string.nc_no_open_conversations_text) + binding.emptyList.emptyListIcon.setImageResource(R.drawable.baseline_info_24) + binding.emptyList.emptyListIcon.visibility = View.VISIBLE + binding.emptyList.emptyListViewText.visibility = View.VISIBLE + } + is OpenConversationsViewModel.FetchConversationsErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + else -> {} + } + } + } + + private fun setupActionBar() { + setSupportActionBar(binding.openConversationsToolbar) + binding.openConversationsToolbar.setNavigationOnClickListener { + if (searching) { + handleSearchUI(false) + searching = false + } else { + onBackPressedDispatcher.onBackPressed() + } + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(R.color.transparent, null).toDrawable()) + viewThemeUtils.material.themeToolbar(binding.openConversationsToolbar) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/adapters/OpenConversationsAdapter.kt b/app/src/main/java/com/nextcloud/talk/openconversations/adapters/OpenConversationsAdapter.kt new file mode 100644 index 0000000..d8a0abb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/openconversations/adapters/OpenConversationsAdapter.kt @@ -0,0 +1,84 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.openconversations.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.RelativeLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.RvItemOpenConversationBinding +import com.nextcloud.talk.extensions.loadConversationAvatar +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.ui.theme.ViewThemeUtils + +class OpenConversationsAdapter( + val user: User, + val viewThemeUtils: ViewThemeUtils, + private val onClick: (Conversation) -> Unit +) : ListAdapter(ConversationsCallback) { + + inner class OpenConversationsViewHolder(val itemBinding: RvItemOpenConversationBinding) : + RecyclerView.ViewHolder(itemBinding.root) { + + var currentConversation: Conversation? = null + + init { + itemBinding.root.setOnClickListener { + currentConversation?.let { + onClick(it) + } + } + } + + fun bindItem(conversation: Conversation) { + val nameTextLayoutParams: RelativeLayout.LayoutParams = itemBinding.nameText.layoutParams as + RelativeLayout.LayoutParams + currentConversation = conversation + val currentConversationModel = ConversationModel.mapToConversationModel(conversation, user) + itemBinding.nameText.text = conversation.displayName + if (conversation.description == "") { + itemBinding.descriptionText.visibility = View.GONE + nameTextLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL) + } else { + itemBinding.descriptionText.text = conversation.description + } + + itemBinding.avatarView.loadConversationAvatar( + user, + currentConversationModel, + false, + viewThemeUtils + ) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OpenConversationsViewHolder = + OpenConversationsViewHolder( + RvItemOpenConversationBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + override fun onBindViewHolder(holder: OpenConversationsViewHolder, position: Int) { + val conversation = getItem(position) + holder.bindItem(conversation) + } +} + +object ConversationsCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Conversation, newItem: Conversation): Boolean = oldItem == newItem + + override fun areContentsTheSame(oldItem: Conversation, newItem: Conversation): Boolean = + oldItem.token == newItem.token +} diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepository.kt new file mode 100644 index 0000000..8842823 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepository.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.openconversations.data + +import com.nextcloud.talk.models.json.conversations.Conversation +import io.reactivex.Observable + +interface OpenConversationsRepository { + + fun fetchConversations(searchTerm: String): Observable> +} diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt new file mode 100644 index 0000000..6bfdddb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/openconversations/data/OpenConversationsRepositoryImpl.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.openconversations.data + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable + +class OpenConversationsRepositoryImpl(private val ncApi: NcApi, currentUserProvider: CurrentUserProviderNew) : + OpenConversationsRepository { + + val currentUser: User = currentUserProvider.currentUser.blockingGet() + val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!! + + val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)) + + override fun fetchConversations(searchTerm: String): Observable> { + val roomOverall = ncApi.getOpenConversations( + credentials, + ApiUtils.getUrlForOpenConversations(apiVersion, currentUser.baseUrl!!), + searchTerm + ) + return roomOverall.map { it.ocs?.data!! } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/viewmodels/OpenConversationsViewModel.kt b/app/src/main/java/com/nextcloud/talk/openconversations/viewmodels/OpenConversationsViewModel.kt new file mode 100644 index 0000000..92501de --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/openconversations/viewmodels/OpenConversationsViewModel.kt @@ -0,0 +1,77 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.openconversations.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.openconversations.data.OpenConversationsRepository +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class OpenConversationsViewModel @Inject constructor(private val repository: OpenConversationsRepository) : + ViewModel() { + + sealed interface ViewState + + object FetchConversationsStartState : ViewState + object FetchConversationsEmptyState : ViewState + object FetchConversationsErrorState : ViewState + open class FetchConversationsSuccessState(val conversations: List) : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(FetchConversationsStartState) + val viewState: LiveData + get() = _viewState + + private val _searchTerm: MutableLiveData = MutableLiveData("") + val searchTerm: LiveData + get() = _searchTerm + + fun fetchConversations() { + _viewState.value = FetchConversationsStartState + repository.fetchConversations(_searchTerm.value ?: "") + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(FetchConversationsObserver()) + } + + fun updateSearchTerm(newTerm: String) { + _searchTerm.value = newTerm + } + + inner class FetchConversationsObserver : Observer> { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(conversations: List) { + if (conversations.isEmpty()) { + _viewState.value = FetchConversationsEmptyState + } else { + _viewState.value = FetchConversationsSuccessState(conversations) + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching open conversations") + _viewState.value = FetchConversationsErrorState + } + + override fun onComplete() { + // unused atm + } + } + + companion object { + private val TAG = OpenConversationsViewModel::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionItem.kt new file mode 100644 index 0000000..c445259 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionItem.kt @@ -0,0 +1,9 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +class PollCreateOptionItem(var pollOption: String) diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt new file mode 100644 index 0000000..e2f7547 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt @@ -0,0 +1,79 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import android.annotation.SuppressLint +import android.text.Editable +import android.text.TextWatcher +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.databinding.PollCreateOptionsItemBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.EmojiTextInputEditText + +class PollCreateOptionViewHolder( + private val binding: PollCreateOptionsItemBinding, + private val viewThemeUtils: ViewThemeUtils +) : RecyclerView.ViewHolder(binding.root) { + + lateinit var optionText: EmojiTextInputEditText + private var textListener: TextWatcher? = null + + @SuppressLint("SetTextI18n") + fun bind( + pollCreateOptionItem: PollCreateOptionItem, + itemsListener: PollCreateOptionsItemListener, + position: Int, + focus: Boolean + ) { + textListener?.let { + binding.pollOptionTextEdit.removeTextChangedListener(it) + } + + binding.pollOptionTextEdit.setText(pollCreateOptionItem.pollOption) + viewThemeUtils.material.colorTextInputLayout(binding.pollOptionTextInputLayout) + + if (focus) { + itemsListener.requestFocus(binding.pollOptionTextEdit) + } + + binding.pollOptionDelete.setOnClickListener { + itemsListener.onRemoveOptionsItemClick(pollCreateOptionItem, position) + } + + textListener = getTextWatcher(pollCreateOptionItem, itemsListener) + binding.pollOptionTextEdit.addTextChangedListener(textListener) + binding.pollOptionTextInputLayout.hint = String.format( + binding.pollOptionTextInputLayout.resources.getString(R.string.polls_option_hint), + position + 1 + ) + + binding.pollOptionDelete.contentDescription = String.format( + binding.pollOptionTextInputLayout.resources.getString(R.string.polls_option_delete), + position + 1 + ) + } + + private fun getTextWatcher( + pollCreateOptionItem: PollCreateOptionItem, + itemsListener: PollCreateOptionsItemListener + ) = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + // unused atm + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // unused atm + } + + override fun onTextChanged(option: CharSequence, start: Int, before: Int, count: Int) { + pollCreateOptionItem.pollOption = option.toString() + + itemsListener.onOptionsItemTextChanged(pollCreateOptionItem) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt new file mode 100644 index 0000000..3284925 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.databinding.PollCreateOptionsItemBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils + +class PollCreateOptionsAdapter( + private val clickListener: PollCreateOptionsItemListener, + private val viewThemeUtils: ViewThemeUtils +) : RecyclerView.Adapter() { + + internal var list: ArrayList = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollCreateOptionViewHolder { + val itemBinding = PollCreateOptionsItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + + return PollCreateOptionViewHolder(itemBinding, viewThemeUtils) + } + + override fun onBindViewHolder(holder: PollCreateOptionViewHolder, position: Int) { + val currentItem = list[position] + var focus = false + + if (list.size - 1 == position && currentItem.pollOption.isBlank()) { + focus = true + } + + holder.bind(currentItem, clickListener, position, focus) + } + + override fun getItemCount(): Int = list.size + + fun updateOptionsList(optionsList: ArrayList) { + list = optionsList + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsItemListener.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsItemListener.kt new file mode 100644 index 0000000..0cf13a2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsItemListener.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import android.widget.EditText + +interface PollCreateOptionsItemListener { + + fun onRemoveOptionsItemClick(pollCreateOptionItem: PollCreateOptionItem, position: Int) + + fun onOptionsItemTextChanged(pollCreateOptionItem: PollCreateOptionItem) + + fun requestFocus(textField: EditText) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderItem.kt new file mode 100644 index 0000000..3e7d202 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderItem.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import com.nextcloud.talk.adapters.items.FlexibleItemViewType + +data class PollResultHeaderItem(val name: String, val percent: Int, val selfVoted: Boolean) : PollResultItem { + + override fun getViewType(): Int = VIEW_TYPE + + companion object { + const val VIEW_TYPE = FlexibleItemViewType.POLL_RESULT_HEADER_ITEM + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt new file mode 100644 index 0000000..7da2b43 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt @@ -0,0 +1,40 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import android.annotation.SuppressLint +import android.graphics.Typeface +import com.nextcloud.talk.databinding.PollResultHeaderItemBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils + +class PollResultHeaderViewHolder( + override val binding: PollResultHeaderItemBinding, + private val viewThemeUtils: ViewThemeUtils +) : PollResultViewHolder(binding) { + + @SuppressLint("SetTextI18n") + override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) { + val item = pollResultItem as PollResultHeaderItem + + viewThemeUtils.material.colorProgressBar(binding.pollOptionBar) + + binding.root.setOnClickListener { clickListener.onClick() } + + binding.pollOptionText.text = item.name + binding.pollOptionPercentText.text = "${item.percent}%" + + viewThemeUtils.dialog.colorDialogSupportingText(binding.pollOptionText) + viewThemeUtils.dialog.colorDialogSupportingText(binding.pollOptionPercentText) + + if (item.selfVoted) { + binding.pollOptionText.setTypeface(null, Typeface.BOLD) + binding.pollOptionPercentText.setTypeface(null, Typeface.BOLD) + } + + binding.pollOptionBar.progress = item.percent + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt new file mode 100644 index 0000000..1266d70 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt @@ -0,0 +1,11 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +interface PollResultItem { + fun getViewType(): Int +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt new file mode 100644 index 0000000..53b5c74 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt @@ -0,0 +1,11 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +interface PollResultItemClickListener { + fun onClick() +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt new file mode 100644 index 0000000..e04033c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +abstract class PollResultViewHolder(open val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) { + abstract fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterItem.kt new file mode 100644 index 0000000..a092c30 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterItem.kt @@ -0,0 +1,19 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import com.nextcloud.talk.adapters.items.FlexibleItemViewType +import com.nextcloud.talk.polls.model.PollDetails + +data class PollResultVoterItem(val details: PollDetails) : PollResultItem { + + override fun getViewType(): Int = VIEW_TYPE + + companion object { + const val VIEW_TYPE = FlexibleItemViewType.POLL_RESULT_VOTER_ITEM + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt new file mode 100644 index 0000000..f0174ee --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt @@ -0,0 +1,70 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import android.annotation.SuppressLint +import android.text.TextUtils +import android.widget.ImageView +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.PollResultVoterItemBinding +import com.nextcloud.talk.extensions.loadFederatedUserAvatar +import com.nextcloud.talk.extensions.loadGuestAvatar +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.polls.model.PollDetails +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.DisplayUtils + +class PollResultVoterViewHolder( + private val user: User, + private val roomToken: String, + override val binding: PollResultVoterItemBinding, + private val viewThemeUtils: ViewThemeUtils +) : PollResultViewHolder(binding) { + + @SuppressLint("SetTextI18n") + override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) { + val item = pollResultItem as PollResultVoterItem + + binding.root.setOnClickListener { clickListener.onClick() } + + binding.pollVoterName.text = item.details.actorDisplayName + loadAvatar(item.details, binding.pollVoterAvatar) + viewThemeUtils.dialog.colorDialogSupportingText(binding.pollVoterName) + } + + private fun loadAvatar(pollDetail: PollDetails, avatar: ImageView) { + when (pollDetail.actorType) { + Participant.ActorType.GUESTS -> { + var displayName = NextcloudTalkApplication.sharedApplication?.resources?.getString(R.string.nc_guest) + if (!TextUtils.isEmpty(pollDetail.actorDisplayName)) { + displayName = pollDetail.actorDisplayName!! + } + avatar.loadGuestAvatar(user, displayName!!, false) + } + + Participant.ActorType.FEDERATED -> { + val darkTheme = if (DisplayUtils.isDarkModeOn(binding.root.context)) 1 else 0 + avatar.loadFederatedUserAvatar( + user, + user.baseUrl!!, + roomToken, + pollDetail.actorId!!, + darkTheme, + false, + false + ) + } + + else -> { + avatar.loadUserAvatar(user, pollDetail.actorId!!, false, false) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewItem.kt new file mode 100644 index 0000000..7273de2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewItem.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import com.nextcloud.talk.adapters.items.FlexibleItemViewType +import com.nextcloud.talk.polls.model.PollDetails + +data class PollResultVotersOverviewItem(val detailsList: List) : PollResultItem { + + override fun getViewType(): Int = VIEW_TYPE + + companion object { + // layout is used as view type for uniqueness + const val VIEW_TYPE = FlexibleItemViewType.POLL_RESULT_VOTERS_OVERVIEW_ITEM + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt new file mode 100644 index 0000000..b23753c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt @@ -0,0 +1,108 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import android.annotation.SuppressLint +import android.text.TextUtils +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.PollResultVotersOverviewItemBinding +import com.nextcloud.talk.extensions.loadFederatedUserAvatar +import com.nextcloud.talk.extensions.loadGuestAvatar +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.polls.model.PollDetails +import com.nextcloud.talk.utils.DisplayUtils + +class PollResultVotersOverviewViewHolder( + private val user: User, + private val roomToken: String, + override val binding: PollResultVotersOverviewItemBinding +) : PollResultViewHolder(binding) { + + @SuppressLint("SetTextI18n") + override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) { + val item = pollResultItem as PollResultVotersOverviewItem + + binding.root.setOnClickListener { clickListener.onClick() } + + val layoutParams = LinearLayout.LayoutParams( + AVATAR_SIZE, + AVATAR_SIZE + ) + + var avatarsToDisplay = MAX_AVATARS + if (item.detailsList.size < avatarsToDisplay) { + avatarsToDisplay = item.detailsList.size + } + val shotsDots = item.detailsList.size > avatarsToDisplay + + for (i in 0 until avatarsToDisplay) { + val pollDetails = item.detailsList[i] + val avatar = ImageView(binding.root.context) + + layoutParams.marginStart = i * AVATAR_OFFSET + avatar.layoutParams = layoutParams + + avatar.translationZ = i.toFloat() * -1 + + loadAvatar(pollDetails, avatar) + + binding.votersAvatarsOverviewWrapper.addView(avatar) + + if (i == avatarsToDisplay - 1 && shotsDots) { + val dotsView = TextView(itemView.context) + layoutParams.marginStart = i * AVATAR_OFFSET + DOTS_OFFSET + dotsView.layoutParams = layoutParams + dotsView.text = DOTS_TEXT + binding.votersAvatarsOverviewWrapper.addView(dotsView) + } + } + } + + private fun loadAvatar(pollDetail: PollDetails, avatar: ImageView) { + when (pollDetail.actorType) { + Participant.ActorType.GUESTS -> { + var displayName = NextcloudTalkApplication.sharedApplication?.resources?.getString(R.string.nc_guest) + if (!TextUtils.isEmpty(pollDetail.actorDisplayName)) { + displayName = pollDetail.actorDisplayName!! + } + avatar.loadGuestAvatar(user, displayName!!, false) + } + + Participant.ActorType.FEDERATED -> { + val darkTheme = if (DisplayUtils.isDarkModeOn(binding.root.context)) 1 else 0 + avatar.loadFederatedUserAvatar( + user, + user.baseUrl!!, + roomToken, + pollDetail.actorId!!, + darkTheme, + false, + false + ) + } + + else -> { + avatar.loadUserAvatar(user, pollDetail.actorId!!, false, false) + } + } + } + + companion object { + const val AVATAR_SIZE = 60 + const val MAX_AVATARS = 10 + const val AVATAR_OFFSET = AVATAR_SIZE - 20 + const val DOTS_OFFSET = 70 + const val DOTS_TEXT = "…" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt new file mode 100644 index 0000000..4af97d1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt @@ -0,0 +1,78 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.PollResultHeaderItemBinding +import com.nextcloud.talk.databinding.PollResultVoterItemBinding +import com.nextcloud.talk.databinding.PollResultVotersOverviewItemBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils + +class PollResultsAdapter( + private val user: User, + private val roomToken: String, + private val clickListener: PollResultItemClickListener, + private val viewThemeUtils: ViewThemeUtils +) : RecyclerView.Adapter() { + internal var list: MutableList = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollResultViewHolder { + var viewHolder: PollResultViewHolder? = null + + when (viewType) { + PollResultHeaderItem.VIEW_TYPE -> { + val itemBinding = PollResultHeaderItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + viewHolder = PollResultHeaderViewHolder(itemBinding, viewThemeUtils) + } + PollResultVoterItem.VIEW_TYPE -> { + val itemBinding = PollResultVoterItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + viewHolder = PollResultVoterViewHolder(user, roomToken, itemBinding, viewThemeUtils) + } + PollResultVotersOverviewItem.VIEW_TYPE -> { + val itemBinding = PollResultVotersOverviewItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + viewHolder = PollResultVotersOverviewViewHolder(user, roomToken, itemBinding) + } + } + return viewHolder!! + } + + override fun onBindViewHolder(holder: PollResultViewHolder, position: Int) { + when (holder.itemViewType) { + PollResultHeaderItem.VIEW_TYPE -> { + val pollResultItem = list[position] + holder.bind(pollResultItem as PollResultHeaderItem, clickListener) + } + PollResultVoterItem.VIEW_TYPE -> { + val pollResultItem = list[position] + holder.bind(pollResultItem as PollResultVoterItem, clickListener) + } + PollResultVotersOverviewItem.VIEW_TYPE -> { + val pollResultItem = list[position] + holder.bind(pollResultItem as PollResultVotersOverviewItem, clickListener) + } + } + } + + override fun getItemCount(): Int = list.size + + override fun getItemViewType(position: Int): Int = list[position].getViewType() +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt b/app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt new file mode 100644 index 0000000..c1a1e98 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.model + +import com.nextcloud.talk.models.json.participants.Participant + +data class Poll( + val id: String, + val question: String?, + val options: List?, + val votes: Map?, + val actorType: Participant.ActorType?, + val actorId: String?, + val actorDisplayName: String?, + val status: Int, + val resultMode: Int, + val maxVotes: Int, + val votedSelf: List?, + val numVoters: Int, + val details: List? +) { + companion object { + const val STATUS_OPEN: Int = 0 + const val STATUS_CLOSED: Int = 1 + const val RESULT_MODE_PUBLIC: Int = 0 + const val RESULT_MODE_HIDDEN: Int = 1 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/model/PollDetails.kt b/app/src/main/java/com/nextcloud/talk/polls/model/PollDetails.kt new file mode 100644 index 0000000..63d704b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/model/PollDetails.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.model + +import com.nextcloud.talk.models.json.participants.Participant + +data class PollDetails( + val actorType: Participant.ActorType?, + val actorId: String?, + val actorDisplayName: String?, + val optionId: Int +) diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepository.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepository.kt new file mode 100644 index 0000000..73332f5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepository.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.repositories + +import com.nextcloud.talk.polls.model.Poll +import io.reactivex.Observable + +interface PollRepository { + + fun createPoll( + roomToken: String, + question: String, + options: List, + resultMode: Int, + maxVotes: Int + ): Observable + + fun getPoll(roomToken: String, pollId: String): Observable + + fun vote(roomToken: String, pollId: String, options: List): Observable + + fun closePoll(roomToken: String, pollId: String): Observable +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt new file mode 100644 index 0000000..a0730bc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt @@ -0,0 +1,115 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.repositories + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.model.PollDetails +import com.nextcloud.talk.polls.repositories.model.PollDetailsResponse +import com.nextcloud.talk.polls.repositories.model.PollResponse +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable +import kotlin.collections.forEach as kForEach + +class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvider: CurrentUserProviderNew) : + PollRepository { + + val currentUser: User = currentUserProvider.currentUser.blockingGet() + val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!! + + override fun createPoll( + roomToken: String, + question: String, + options: List, + resultMode: Int, + maxVotes: Int + ): Observable = + ncApi.createPoll( + credentials, + ApiUtils.getUrlForPoll( + currentUser.baseUrl!!, + roomToken + ), + question, + options, + resultMode, + maxVotes + ).map { mapToPoll(it.ocs?.data!!) } + + override fun getPoll(roomToken: String, pollId: String): Observable = + ncApi.getPoll( + credentials, + ApiUtils.getUrlForPoll( + currentUser.baseUrl!!, + roomToken, + pollId + ) + ).map { mapToPoll(it.ocs?.data!!) } + + override fun vote(roomToken: String, pollId: String, options: List): Observable = + ncApi.votePoll( + credentials, + ApiUtils.getUrlForPoll( + currentUser.baseUrl!!, + roomToken, + pollId + ), + options + ).map { mapToPoll(it.ocs?.data!!) } + + override fun closePoll(roomToken: String, pollId: String): Observable = + ncApi.closePoll( + credentials, + ApiUtils.getUrlForPoll( + currentUser.baseUrl!!, + roomToken, + pollId + ) + ).map { mapToPoll(it.ocs?.data!!) } + + companion object { + + private fun mapToPoll(pollResponse: PollResponse): Poll { + val pollDetails = pollResponse.details?.map { it -> mapToPollDetails(it) } + + return Poll( + pollResponse.id, + pollResponse.question, + pollResponse.options, + convertVotes(pollResponse.votes), + pollResponse.actorType, + pollResponse.actorId, + pollResponse.actorDisplayName, + pollResponse.status, + pollResponse.resultMode, + pollResponse.maxVotes, + pollResponse.votedSelf, + pollResponse.numVoters, + pollDetails + ) + } + + private fun convertVotes(votes: Map?): Map { + val resultMap: MutableMap = HashMap() + votes?.kForEach { (key, value) -> + resultMap[key.replace("option-", "")] = value + } + return resultMap + } + + private fun mapToPollDetails(pollDetailsResponse: PollDetailsResponse): PollDetails = + PollDetails( + pollDetailsResponse.actorType, + pollDetailsResponse.actorId, + pollDetailsResponse.actorDisplayName, + pollDetailsResponse.optionId + ) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollDetailsResponse.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollDetailsResponse.kt new file mode 100644 index 0000000..24e8fa5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollDetailsResponse.kt @@ -0,0 +1,33 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter +import com.nextcloud.talk.models.json.participants.Participant +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PollDetailsResponse( + @JsonField(name = ["actorType"], typeConverter = EnumActorTypeConverter::class) + var actorType: Participant.ActorType? = null, + + @JsonField(name = ["actorId"]) + var actorId: String, + + @JsonField(name = ["actorDisplayName"]) + var actorDisplayName: String, + + @JsonField(name = ["optionId"]) + var optionId: Int +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, "", "", 0) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOCS.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOCS.kt new file mode 100644 index 0000000..ff8dbfd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOCS.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PollOCS( + @JsonField(name = ["data"]) + var data: PollResponse? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOverall.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOverall.kt new file mode 100644 index 0000000..7f44dc4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PollOverall( + @JsonField(name = ["ocs"]) + var ocs: PollOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt new file mode 100644 index 0000000..e406fbb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt @@ -0,0 +1,60 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter +import com.nextcloud.talk.models.json.participants.Participant +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class PollResponse( + @JsonField(name = ["id"]) + var id: String, + + @JsonField(name = ["question"]) + var question: String? = null, + + @JsonField(name = ["options"]) + var options: ArrayList? = null, + + @JsonField(name = ["votes"]) + var votes: Map? = null, + + @JsonField(name = ["actorType"], typeConverter = EnumActorTypeConverter::class) + var actorType: Participant.ActorType? = null, + + @JsonField(name = ["actorId"]) + var actorId: String? = null, + + @JsonField(name = ["actorDisplayName"]) + var actorDisplayName: String? = null, + + @JsonField(name = ["status"]) + var status: Int = 0, + + @JsonField(name = ["resultMode"]) + var resultMode: Int = 0, + + @JsonField(name = ["maxVotes"]) + var maxVotes: Int = 0, + + @JsonField(name = ["votedSelf"]) + var votedSelf: ArrayList? = null, + + @JsonField(name = ["numVoters"]) + var numVoters: Int = 0, + + @JsonField(name = ["details"]) + var details: ArrayList? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this("id", null, null, null, null, null, null, 0, 0, 0, null, 0, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt new file mode 100644 index 0000000..0d6dfaa --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt @@ -0,0 +1,197 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.ui + +import android.annotation.SuppressLint +import android.app.Dialog +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogPollCreateBinding +import com.nextcloud.talk.polls.adapters.PollCreateOptionItem +import com.nextcloud.talk.polls.adapters.PollCreateOptionsAdapter +import com.nextcloud.talk.polls.adapters.PollCreateOptionsItemListener +import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PollCreateDialogFragment : + DialogFragment(), + PollCreateOptionsItemListener { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var binding: DialogPollCreateBinding + private lateinit var viewModel: PollCreateViewModel + + private var adapter: PollCreateOptionsAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + viewModel = ViewModelProvider(this, viewModelFactory)[PollCreateViewModel::class.java] + val roomToken = arguments?.getString(KEY_ROOM_TOKEN)!! + viewModel.setData(roomToken) + } + + @SuppressLint("InflateParams") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogPollCreateBinding.inflate(layoutInflater) + + val dialogBuilder = MaterialAlertDialogBuilder(binding.root.context) + .setView(binding.root) + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.root.context, dialogBuilder) + + return dialogBuilder.create() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + binding.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.options.observe(viewLifecycleOwner) { options -> adapter?.updateOptionsList(options) } + + binding.pollCreateOptionsList.layoutManager = LinearLayoutManager(context) + + adapter = PollCreateOptionsAdapter(this, viewThemeUtils) + binding.pollCreateOptionsList.adapter = adapter + + themeDialog() + + setupListeners() + setupStateObserver() + } + + private fun themeDialog() { + viewThemeUtils.platform.colorTextView(binding.pollQuestion) + viewThemeUtils.platform.colorTextView(binding.pollOptions) + viewThemeUtils.platform.colorTextView(binding.pollSettings) + + viewThemeUtils.material.colorTextInputLayout(binding.pollCreateQuestionTextInputLayout) + + viewThemeUtils.material.colorMaterialButtonText(binding.pollAddOptionsItem) + viewThemeUtils.material.colorMaterialButtonText(binding.pollDismiss) + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.pollCreateButton) + + viewThemeUtils.platform.themeCheckbox(binding.pollPrivatePollCheckbox) + viewThemeUtils.platform.themeCheckbox(binding.pollMultipleAnswersCheckbox) + } + + private fun setupListeners() { + binding.pollAddOptionsItem.setOnClickListener { + viewModel.addOption() + adapter?.itemCount?.minus(1)?.let { binding.pollCreateOptionsList.scrollToPosition(it) } + } + + binding.pollDismiss.setOnClickListener { + dismiss() + } + + binding.pollCreateQuestionTextEdit.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable) { + // unused atm + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // unused atm + } + + override fun onTextChanged(question: CharSequence, start: Int, before: Int, count: Int) { + if (question.toString() != viewModel.question) { + viewModel.setQuestion(question.toString()) + } + } + }) + + binding.pollPrivatePollCheckbox.setOnClickListener { + viewModel.setPrivatePoll(binding.pollPrivatePollCheckbox.isChecked) + } + + binding.pollMultipleAnswersCheckbox.setOnClickListener { + viewModel.setMultipleAnswer(binding.pollMultipleAnswersCheckbox.isChecked) + } + + binding.pollCreateButton.setOnClickListener { + viewModel.createPoll() + } + } + + private fun setupStateObserver() { + viewModel.viewState.observe(viewLifecycleOwner) { state -> + when (state) { + is PollCreateViewModel.PollCreatedState -> dismiss() + is PollCreateViewModel.PollCreationFailedState -> showError() + is PollCreateViewModel.PollCreationState -> updateButtons(state) + } + } + } + + private fun updateButtons(state: PollCreateViewModel.PollCreationState) { + binding.pollAddOptionsItem.isEnabled = state.enableAddOptionButton + binding.pollCreateButton.isEnabled = state.enableCreatePollButton + } + + private fun showError() { + dismiss() + Log.e(TAG, "Failed to create poll") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + override fun onRemoveOptionsItemClick(pollCreateOptionItem: PollCreateOptionItem, position: Int) { + viewModel.removeOption(pollCreateOptionItem) + } + + override fun onOptionsItemTextChanged(pollCreateOptionItem: PollCreateOptionItem) { + viewModel.optionsItemTextChanged() + } + + override fun requestFocus(textField: EditText) { + if (binding.pollCreateQuestionTextEdit.text?.isBlank() == true) { + binding.pollCreateQuestionTextEdit.requestFocus() + } else { + textField.requestFocus() + } + } + + /** + * Fragment creator + */ + companion object { + private val TAG = PollCreateDialogFragment::class.java.simpleName + private const val KEY_ROOM_TOKEN = "keyRoomToken" + + @JvmStatic + fun newInstance(roomTokenParam: String): PollCreateDialogFragment { + val args = Bundle() + args.putString(KEY_ROOM_TOKEN, roomTokenParam) + val fragment = PollCreateDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt new file mode 100644 index 0000000..75c65a7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt @@ -0,0 +1,47 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import autodagger.AutoInjector +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogPollLoadingBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PollLoadingFragment : Fragment() { + + private lateinit var binding: DialogPollLoadingBinding + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = DialogPollLoadingBinding.inflate(inflater, container, false) + binding.root.layoutParams.height = HEIGHT + viewThemeUtils.platform.colorCircularProgressBar(binding.pollLoadingProgressbar, ColorRole.PRIMARY) + return binding.root + } + + companion object { + private const val HEIGHT = 300 + + @JvmStatic + fun newInstance(): PollLoadingFragment = PollLoadingFragment() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt new file mode 100644 index 0000000..0019706 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt @@ -0,0 +1,188 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.ui + +import android.annotation.SuppressLint +import android.app.Dialog +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.DialogFragment +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.DialogPollMainBinding +import com.nextcloud.talk.polls.viewmodels.PollMainViewModel +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PollMainDialogFragment : DialogFragment() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + var currentUserProvider: CurrentUserProviderNew? = null + @Inject set + + private lateinit var binding: DialogPollMainBinding + private lateinit var viewModel: PollMainViewModel + + lateinit var user: User + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + viewModel = ViewModelProvider(this, viewModelFactory)[PollMainViewModel::class.java] + + user = currentUserProvider?.currentUser?.blockingGet()!! + + val roomToken = arguments?.getString(KEY_ROOM_TOKEN)!! + val isOwnerOrModerator = arguments?.getBoolean(KEY_OWNER_OR_MODERATOR)!! + val pollId = arguments?.getString(KEY_POLL_ID)!! + val pollTitle = arguments?.getString(KEY_POLL_TITLE)!! + + viewModel.setData(user, roomToken, isOwnerOrModerator, pollId, pollTitle) + } + + @SuppressLint("InflateParams") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogPollMainBinding.inflate(layoutInflater) + + val dialogBuilder = MaterialAlertDialogBuilder(binding.root.context).setView(binding.root) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.root.context, dialogBuilder) + + val dialog = dialogBuilder.create() + + binding.messagePollTitle.text = viewModel.pollTitle + viewThemeUtils.dialog.colorDialogHeadline(binding.messagePollTitle) + viewThemeUtils.dialog.colorDialogIcon(binding.messagePollIcon) + + return dialog + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + binding.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.viewState.observe(viewLifecycleOwner) { state -> + when (state) { + PollMainViewModel.InitialState -> {} + is PollMainViewModel.PollVoteState -> { + initVotersAmount(state.showVotersAmount, state.poll.numVoters, false) + showVoteScreen() + } + + is PollMainViewModel.PollResultState -> { + initVotersAmount(state.showVotersAmount, state.poll.numVoters, true) + showResultsScreen() + } + + is PollMainViewModel.LoadingState -> { + showLoadingScreen() + } + + is PollMainViewModel.DismissDialogState -> { + dismiss() + } + + else -> {} + } + } + } + + private fun showLoadingScreen() { + val contentFragment = PollLoadingFragment.newInstance() + val transaction = childFragmentManager.beginTransaction() + transaction.replace(binding.messagePollContentFragment.id, contentFragment) + transaction.commit() + } + + private fun showVoteScreen() { + val contentFragment = PollVoteFragment.newInstance() + + val transaction = childFragmentManager.beginTransaction() + transaction.replace(binding.messagePollContentFragment.id, contentFragment) + transaction.commit() + } + + private fun showResultsScreen() { + val contentFragment = PollResultsFragment.newInstance() + + val transaction = childFragmentManager.beginTransaction() + transaction.replace(binding.messagePollContentFragment.id, contentFragment) + transaction.commit() + } + + private fun initVotersAmount(showVotersAmount: Boolean, numVoters: Int, showResultSubtitle: Boolean) { + if (showVotersAmount) { + viewThemeUtils.dialog.colorDialogSupportingText(binding.pollVotesAmount) + binding.pollVotesAmount.visibility = View.VISIBLE + binding.pollVotesAmount.text = resources.getQuantityString( + R.plurals.polls_amount_voters, + numVoters, + numVoters + ) + } else { + binding.pollVotesAmount.visibility = View.GONE + } + + if (showResultSubtitle) { + viewThemeUtils.dialog.colorDialogSupportingText(binding.pollResultsSubtitle) + binding.pollResultsSubtitle.visibility = View.VISIBLE + binding.pollResultsSubtitleSeperator.visibility = View.VISIBLE + } else { + binding.pollResultsSubtitle.visibility = View.GONE + binding.pollResultsSubtitleSeperator.visibility = View.GONE + } + } + + /** + * Fragment creator + */ + companion object { + private const val KEY_USER_ENTITY = "keyUserEntity" + private const val KEY_ROOM_TOKEN = "keyRoomToken" + private const val KEY_OWNER_OR_MODERATOR = "keyIsOwnerOrModerator" + private const val KEY_POLL_ID = "keyPollId" + private const val KEY_POLL_TITLE = "keyPollTitle" + + @JvmStatic + fun newInstance( + user: User, + roomTokenParam: String, + isOwnerOrModerator: Boolean, + pollId: String, + name: String + ): PollMainDialogFragment { + val args = bundleOf( + KEY_USER_ENTITY to user, + KEY_ROOM_TOKEN to roomTokenParam, + KEY_OWNER_OR_MODERATOR to isOwnerOrModerator, + KEY_POLL_ID to pollId, + KEY_POLL_TITLE to name + ) + + val fragment = PollMainDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt new file mode 100644 index 0000000..ae925ca --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt @@ -0,0 +1,154 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogPollResultsBinding +import com.nextcloud.talk.polls.adapters.PollResultItemClickListener +import com.nextcloud.talk.polls.adapters.PollResultsAdapter +import com.nextcloud.talk.polls.viewmodels.PollMainViewModel +import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PollResultsFragment : + Fragment(), + PollResultItemClickListener { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var parentViewModel: PollMainViewModel + lateinit var viewModel: PollResultsViewModel + + lateinit var binding: DialogPollResultsBinding + + private var adapter: PollResultsAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + viewModel = ViewModelProvider(this, viewModelFactory)[PollResultsViewModel::class.java] + parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory)[PollMainViewModel::class.java] + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = DialogPollResultsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + parentViewModel.viewState.observe(viewLifecycleOwner) { state -> + if (state is PollMainViewModel.PollResultState) { + initAdapter() + viewModel.setPoll(state.poll) + initEditButton(state.showEditButton) + initEndPollButton(state.showEndPollButton) + } + } + + viewModel.items.observe(viewLifecycleOwner) { + val adapter = PollResultsAdapter( + parentViewModel.user, + parentViewModel.roomToken, + this, + viewThemeUtils + ) + .apply { + if (it != null) { + list = it + } + } + binding.pollResultsList.adapter = adapter + } + + themeDialog() + } + + private fun themeDialog() { + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.editVoteButton) + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.pollResultsEndPollButton) + } + + private fun initAdapter() { + adapter = PollResultsAdapter( + parentViewModel.user, + parentViewModel.roomToken, + this, + viewThemeUtils + ) + binding.pollResultsList.adapter = adapter + binding.pollResultsList.layoutManager = LinearLayoutManager(context) + } + + private fun initEditButton(showEditButton: Boolean) { + if (showEditButton) { + binding.editVoteButton.visibility = View.VISIBLE + binding.editVoteButton.setOnClickListener { + parentViewModel.editVotes() + } + } else { + binding.editVoteButton.visibility = View.GONE + } + } + + private fun initEndPollButton(showEndPollButton: Boolean) { + if (showEndPollButton) { + binding.pollResultsEndPollButton.visibility = View.VISIBLE + binding.pollResultsEndPollButton.setOnClickListener { + val dialogBuilder = MaterialAlertDialogBuilder(binding.pollResultsEndPollButton.context) + .setTitle(R.string.polls_end_poll) + .setMessage(R.string.polls_end_poll_confirm) + .setPositiveButton(R.string.polls_end_poll) { _, _ -> + parentViewModel.endPoll() + } + .setNegativeButton(R.string.nc_cancel, null) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground( + binding.pollResultsEndPollButton.context, + dialogBuilder + ) + + val dialog = dialogBuilder.show() + + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } else { + binding.pollResultsEndPollButton.visibility = View.GONE + } + } + + override fun onClick() { + viewModel.toggleDetails() + } + + companion object { + @JvmStatic + fun newInstance(): PollResultsFragment = PollResultsFragment() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt new file mode 100644 index 0000000..38cf603 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt @@ -0,0 +1,215 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.ui + +import android.graphics.Typeface +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.CompoundButton +import android.widget.RadioButton +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogPollVoteBinding +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.viewmodels.PollMainViewModel +import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PollVoteFragment : Fragment() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var parentViewModel: PollMainViewModel + lateinit var viewModel: PollVoteViewModel + + private lateinit var binding: DialogPollVoteBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + viewModel = ViewModelProvider(this, viewModelFactory)[PollVoteViewModel::class.java] + + parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory)[PollMainViewModel::class.java] + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = DialogPollVoteBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + parentViewModel.viewState.observe(viewLifecycleOwner) { state -> + if (state is PollMainViewModel.PollVoteState) { + initPollOptions(state.poll) + initEndPollButton(state.showEndPollButton) + updateSubmitButton() + updateDismissEditButton(state.showDismissEditButton) + } + } + + viewModel.viewState.observe(viewLifecycleOwner) { state -> + when (state) { + PollVoteViewModel.InitialState -> {} + is PollVoteViewModel.PollVoteFailedState -> { + Log.e(TAG, "Failed to vote on poll.") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + is PollVoteViewModel.PollVoteHiddenSuccessState -> { + Snackbar.make(binding.root, R.string.polls_voted_hidden_success, Snackbar.LENGTH_LONG).show() + parentViewModel.dismissDialog() + } + is PollVoteViewModel.PollVoteSuccessState -> { + parentViewModel.voted() + } + } + } + + viewModel.submitButtonEnabled.observe(viewLifecycleOwner) { enabled -> + binding.pollVoteSubmitButton.isEnabled = enabled + } + + binding.pollVoteRadioGroup.setOnCheckedChangeListener { _, checkedId -> + viewModel.selectOption(checkedId, true) + updateSubmitButton() + } + + binding.pollVoteSubmitButton.setOnClickListener { + viewModel.vote(parentViewModel.roomToken, parentViewModel.pollId) + } + + binding.pollVoteEditDismiss.setOnClickListener { + parentViewModel.dismissEditVotes() + } + + themeDialog() + } + + private fun themeDialog() { + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.pollVoteSubmitButton) + viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.pollVoteEndPollButton) + viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.pollVoteEditDismiss) + } + + private fun updateDismissEditButton(showDismissEditButton: Boolean) { + if (showDismissEditButton) { + binding.pollVoteEditDismiss.visibility = View.VISIBLE + } else { + binding.pollVoteEditDismiss.visibility = View.GONE + } + } + + private fun initPollOptions(poll: Poll) { + poll.votedSelf?.let { viewModel.initVotedOptions(it as ArrayList) } + + if (poll.maxVotes == 1) { + binding.pollVoteRadioGroup.removeAllViews() + poll.options?.map { option -> + RadioButton(context).apply { text = option } + }?.forEachIndexed { index, radioButton -> + radioButton.id = index + viewThemeUtils.platform.themeRadioButton(radioButton) + makeOptionBoldIfSelfVoted(radioButton, poll, index) + binding.pollVoteRadioGroup.addView(radioButton) + + radioButton.isChecked = viewModel.selectedOptions.contains(index) == true + } + } else { + binding.voteOptionsCheckboxesWrapper.removeAllViews() + + poll.options?.map { option -> + CheckBox(context).apply { + text = option + } + }?.forEachIndexed { index, checkBox -> + viewThemeUtils.platform.themeCheckbox(checkBox) + checkBox.id = index + makeOptionBoldIfSelfVoted(checkBox, poll, index) + binding.voteOptionsCheckboxesWrapper.addView(checkBox) + + checkBox.isChecked = viewModel.selectedOptions.contains(index) == true + checkBox.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + if (poll.maxVotes == UNLIMITED_VOTES || viewModel.selectedOptions.size < poll.maxVotes) { + viewModel.selectOption(index, false) + } else { + checkBox.isChecked = false + Snackbar.make(binding.root, R.string.polls_max_votes_reached, Snackbar.LENGTH_LONG).show() + } + } else { + viewModel.deSelectOption(index) + } + updateSubmitButton() + } + } + } + } + + private fun updateSubmitButton() { + viewModel.updateSubmitButton() + } + + private fun makeOptionBoldIfSelfVoted(button: CompoundButton, poll: Poll, index: Int) { + if (poll.votedSelf?.contains(index) == true) { + button.setTypeface(null, Typeface.BOLD) + } + } + + private fun initEndPollButton(showEndPollButton: Boolean) { + if (showEndPollButton) { + binding.pollVoteEndPollButton.visibility = View.VISIBLE + binding.pollVoteEndPollButton.setOnClickListener { + val dialogBuilder = MaterialAlertDialogBuilder(binding.pollVoteEndPollButton.context) + .setTitle(R.string.polls_end_poll) + .setMessage(R.string.polls_end_poll_confirm) + .setPositiveButton(R.string.polls_end_poll) { _, _ -> + parentViewModel.endPoll() + } + .setNegativeButton(R.string.nc_cancel, null) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground( + binding.pollVoteEndPollButton.context, + dialogBuilder + ) + + val dialog = dialogBuilder.show() + + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } else { + binding.pollVoteEndPollButton.visibility = View.GONE + } + } + + companion object { + private val TAG = PollVoteFragment::class.java.simpleName + private const val UNLIMITED_VOTES = 0 + + @JvmStatic + fun newInstance(): PollVoteFragment = PollVoteFragment() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt new file mode 100644 index 0000000..b0a3b2b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt @@ -0,0 +1,191 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.polls.adapters.PollCreateOptionItem +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.repositories.PollRepository +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class PollCreateViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() { + + private lateinit var roomToken: String + + sealed interface ViewState + open class PollCreationState(val enableAddOptionButton: Boolean, val enableCreatePollButton: Boolean) : ViewState + object PollCreatedState : ViewState + object PollCreationFailedState : ViewState + + private val _viewState: MutableLiveData = MutableLiveData( + PollCreationState( + enableAddOptionButton = true, + enableCreatePollButton = false + ) + ) + val viewState: LiveData + get() = _viewState + + private var _options: MutableLiveData> = + MutableLiveData>() + val options: LiveData> + get() = _options + + private var _question: String = "" + val question: String + get() = _question + + private var _privatePoll: Boolean = false + val privatePoll: Boolean + get() = _privatePoll + + private var _multipleAnswer: Boolean = false + val multipleAnswer: Boolean + get() = _multipleAnswer + + private var disposable: Disposable? = null + + init { + addOption() + addOption() + } + + fun setData(roomToken: String) { + this.roomToken = roomToken + updateCreationState() + } + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + fun addOption() { + val item = PollCreateOptionItem("") + val currentOptions: ArrayList = _options.value ?: ArrayList() + currentOptions.add(item) + _options.value = currentOptions + updateCreationState() + } + + fun removeOption(item: PollCreateOptionItem) { + val currentOptions: ArrayList = _options.value ?: ArrayList() + currentOptions.remove(item) + _options.value = currentOptions + updateCreationState() + } + + fun createPoll() { + var maxVotes = 1 + if (multipleAnswer) { + maxVotes = 0 + } + + var resultMode = 0 + if (privatePoll) { + resultMode = 1 + } + + _options.value = _options.value?.filter { it.pollOption.isNotEmpty() } as ArrayList + + if (_question.isNotEmpty() && _options.value?.isNotEmpty() == true) { + _viewState.value = PollCreationState(enableAddOptionButton = false, enableCreatePollButton = false) + + repository.createPoll( + roomToken, + _question, + _options.value!!.map { it.pollOption }, + resultMode, + maxVotes + ) + .doOnSubscribe { disposable = it } + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(PollObserver()) + } + } + + fun setQuestion(question: String) { + _question = question + updateCreationState() + } + + fun setPrivatePoll(checked: Boolean) { + _privatePoll = checked + } + + fun setMultipleAnswer(checked: Boolean) { + _multipleAnswer = checked + } + + fun optionsItemTextChanged() { + updateCreationState() + } + + private fun updateCreationState() { + _viewState.value = PollCreationState(enableAddOptionButton(), enableCreatePollButton()) + } + + private fun enableCreatePollButton(): Boolean = _question.isNotEmpty() && atLeastTwoOptionsAreFilled() + + private fun atLeastTwoOptionsAreFilled(): Boolean { + if (_options.value != null) { + var filledOptions = 0 + _options.value?.forEach { + if (it.pollOption.isNotEmpty()) { + filledOptions++ + } + if (filledOptions >= 2) { + return true + } + } + } + return false + } + + private fun enableAddOptionButton(): Boolean { + if (_options.value != null && _options.value?.size != 0) { + _options.value?.forEach { + if (it.pollOption.isBlank()) { + return false + } + } + } + return true + } + + inner class PollObserver : Observer { + + lateinit var poll: Poll + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(response: Poll) { + poll = response + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to create poll", e) + _viewState.value = PollCreationFailedState + } + + override fun onComplete() { + _viewState.value = PollCreatedState + } + } + + companion object { + private val TAG = PollCreateViewModel::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt new file mode 100644 index 0000000..c706159 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt @@ -0,0 +1,173 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.repositories.PollRepository +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class PollMainViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() { + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + lateinit var user: User + lateinit var roomToken: String + private var isOwnerOrModerator: Boolean = false + lateinit var pollId: String + lateinit var pollTitle: String + + private var editVotes: Boolean = false + + sealed interface ViewState + object InitialState : ViewState + object DismissDialogState : ViewState + object LoadingState : ViewState + + open class PollVoteState( + val poll: Poll, + val showVotersAmount: Boolean, + val showEndPollButton: Boolean, + val showDismissEditButton: Boolean + ) : ViewState + + open class PollResultState( + val poll: Poll, + val showVotersAmount: Boolean, + val showEndPollButton: Boolean, + val showEditButton: Boolean + ) : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(InitialState) + val viewState: LiveData + get() = _viewState + + private var disposable: Disposable? = null + + fun setData(user: User, roomToken: String, isOwnerOrModerator: Boolean, pollId: String, pollTitle: String) { + this.user = user + this.roomToken = roomToken + this.isOwnerOrModerator = isOwnerOrModerator + this.pollId = pollId + this.pollTitle = pollTitle + + loadPoll() + } + + fun voted() { + loadPoll() + } + + fun editVotes() { + editVotes = true + loadPoll() + } + + fun dismissEditVotes() { + loadPoll() + } + + private fun loadPoll() { + _viewState.value = LoadingState + repository.getPoll(roomToken, pollId) + .doOnSubscribe { disposable = it } + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(PollObserver()) + } + + fun endPoll() { + _viewState.value = LoadingState + repository.closePoll(roomToken, pollId) + .doOnSubscribe { disposable = it } + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(PollObserver()) + } + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + inner class PollObserver : Observer { + + lateinit var poll: Poll + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(response: Poll) { + poll = response + } + + override fun onError(e: Throwable) { + Log.e(TAG, "An error occurred: $e") + } + + override fun onComplete() { + val showEndPollButton = showEndPollButton(poll) + val showVotersAmount = showVotersAmount(poll) + + if (votedForOpenHiddenPoll(poll)) { + _viewState.value = PollVoteState(poll, showVotersAmount, showEndPollButton, false) + } else if (editVotes && poll.status == Poll.STATUS_OPEN) { + _viewState.value = PollVoteState(poll, false, showEndPollButton, true) + editVotes = false + } else if (poll.status == Poll.STATUS_CLOSED || poll.votedSelf?.isNotEmpty() == true) { + val showEditButton = poll.status == Poll.STATUS_OPEN && poll.resultMode == Poll.RESULT_MODE_PUBLIC + _viewState.value = PollResultState(poll, showVotersAmount, showEndPollButton, showEditButton) + } else if (poll.votedSelf.isNullOrEmpty()) { + _viewState.value = PollVoteState(poll, showVotersAmount, showEndPollButton, false) + } else { + Log.w(TAG, "unknown poll state") + } + } + } + + private fun showEndPollButton(poll: Poll): Boolean = + !editVotes && poll.status == Poll.STATUS_OPEN && (isPollCreatedByCurrentUser(poll) || isOwnerOrModerator) + + private fun showVotersAmount(poll: Poll): Boolean = + votedForPublicPoll(poll) || + poll.status == Poll.STATUS_CLOSED || + isOwnerOrModerator || + isPollCreatedByCurrentUser(poll) + + private fun votedForOpenHiddenPoll(poll: Poll): Boolean = + poll.status == Poll.STATUS_OPEN && + poll.resultMode == Poll.RESULT_MODE_HIDDEN && + poll.votedSelf?.isNotEmpty() == true + + private fun votedForPublicPoll(poll: Poll): Boolean = + poll.resultMode == Poll.RESULT_MODE_PUBLIC && + poll.votedSelf?.isNotEmpty() == true + + private fun isPollCreatedByCurrentUser(poll: Poll): Boolean = + currentUserProvider.currentUser.blockingGet().userId == poll.actorId + + fun dismissDialog() { + _viewState.value = DismissDialogState + } + + companion object { + private val TAG = PollMainViewModel::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt new file mode 100644 index 0000000..f924b27 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt @@ -0,0 +1,111 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.polls.adapters.PollResultHeaderItem +import com.nextcloud.talk.polls.adapters.PollResultItem +import com.nextcloud.talk.polls.adapters.PollResultVoterItem +import com.nextcloud.talk.polls.adapters.PollResultVotersOverviewItem +import com.nextcloud.talk.polls.model.Poll +import io.reactivex.disposables.Disposable +import javax.inject.Inject + +class PollResultsViewModel @Inject constructor() : ViewModel() { + + sealed interface ViewState + object InitialState : ViewState + + private var _poll: Poll? = null + val poll: Poll? + get() = _poll + + private var itemsOverviewList: ArrayList = ArrayList() + private var itemsDetailsList: ArrayList = ArrayList() + + private var _items: MutableLiveData?> = MutableLiveData?>() + val items: MutableLiveData?> + get() = _items + + private var disposable: Disposable? = null + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + fun setPoll(poll: Poll) { + _poll = poll + initPollResults(_poll!!) + } + + private fun initPollResults(poll: Poll) { + _items.value = ArrayList() + + var oneVoteInPercent = 0 + if (poll.numVoters != 0) { + oneVoteInPercent = HUNDRED / poll.numVoters + } + + poll.options?.forEachIndexed { index, option -> + val votersAmountForThisOption = getVotersAmountForOption(poll, index) + val optionsPercent = oneVoteInPercent * votersAmountForThisOption + + val pollResultHeaderItem = PollResultHeaderItem( + option, + optionsPercent, + isOptionSelfVoted(poll, index) + ) + itemsOverviewList.add(pollResultHeaderItem) + itemsDetailsList.add(pollResultHeaderItem) + + val voters = poll.details?.filter { it.optionId == index } + + if (!voters.isNullOrEmpty()) { + itemsOverviewList.add(PollResultVotersOverviewItem(voters)) + } + + if (!voters.isNullOrEmpty()) { + voters.forEach { + itemsDetailsList.add(PollResultVoterItem(it)) + } + } + } + + _items.value = itemsOverviewList + } + + private fun getVotersAmountForOption(poll: Poll, index: Int): Int { + var votersAmountForThisOption: Int? = 0 + if (poll.details != null) { + votersAmountForThisOption = poll.details.filter { it.optionId == index }.size + } else if (poll.votes != null) { + votersAmountForThisOption = poll.votes.filter { it.key.toInt() == index }[index.toString()] + if (votersAmountForThisOption == null) { + votersAmountForThisOption = 0 + } + } + return votersAmountForThisOption!! + } + + private fun isOptionSelfVoted(poll: Poll, index: Int): Boolean = poll.votedSelf?.contains(index) == true + + fun toggleDetails() { + if (_items.value?.containsAll(itemsDetailsList) == true) { + _items.value = itemsOverviewList + } else { + _items.value = itemsDetailsList + } + } + + companion object { + private val TAG = PollResultsViewModel::class.java.simpleName + private const val HUNDRED = 100 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt new file mode 100644 index 0000000..499cd31 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt @@ -0,0 +1,118 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.polls.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.repositories.PollRepository +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class PollVoteViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() { + + sealed interface ViewState + object InitialState : ViewState + open class PollVoteSuccessState : ViewState + open class PollVoteHiddenSuccessState : ViewState + open class PollVoteFailedState : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(InitialState) + val viewState: LiveData + get() = _viewState + + private val _submitButtonEnabled: MutableLiveData = MutableLiveData() + val submitButtonEnabled: LiveData + get() = _submitButtonEnabled + + private var disposable: Disposable? = null + + private var _votedOptions: List = emptyList() + val votedOptions: List + get() = _votedOptions + + private var _selectedOptions: List = emptyList() + val selectedOptions: List + get() = _selectedOptions + + fun initVotedOptions(selectedOptions: List) { + _votedOptions = selectedOptions + _selectedOptions = selectedOptions + } + + fun selectOption(option: Int, isRadioBox: Boolean) { + _selectedOptions = if (isRadioBox) { + listOf(option) + } else { + _selectedOptions.plus(option) + } + } + + fun deSelectOption(option: Int) { + _selectedOptions = _selectedOptions.minus(option) + } + + fun vote(roomToken: String, pollId: String) { + if (_selectedOptions.isNotEmpty()) { + _submitButtonEnabled.value = false + + repository.vote(roomToken, pollId, _selectedOptions) + .doOnSubscribe { disposable = it } + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(PollObserver()) + } + } + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + fun updateSubmitButton() { + val areSelectedOptionsDifferentToVotedOptions = !( + votedOptions.containsAll(selectedOptions) && + selectedOptions.containsAll(votedOptions) + ) + + _submitButtonEnabled.value = areSelectedOptionsDifferentToVotedOptions && selectedOptions.isNotEmpty() + } + + inner class PollObserver : Observer { + + lateinit var poll: Poll + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(response: Poll) { + poll = response + } + + override fun onError(e: Throwable) { + Log.e(TAG, "An error occurred: $e") + _viewState.value = PollVoteFailedState() + } + + override fun onComplete() { + if (poll.resultMode == 1) { + _viewState.value = PollVoteHiddenSuccessState() + } else { + _viewState.value = PollVoteSuccessState() + } + } + } + + companion object { + private val TAG = PollVoteViewModel::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java b/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java new file mode 100644 index 0000000..b014cae --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/presenters/MentionAutocompletePresenter.java @@ -0,0 +1,194 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.presenters; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import com.nextcloud.talk.adapters.items.MentionAutocompleteItem; +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.models.json.mention.Mention; +import com.nextcloud.talk.models.json.mention.MentionOverall; +import com.nextcloud.talk.ui.theme.ViewThemeUtils; +import com.nextcloud.talk.users.UserManager; +import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew; +import com.otaliastudios.autocomplete.RecyclerViewPresenter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; +import autodagger.AutoInjector; +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +@AutoInjector(NextcloudTalkApplication.class) +public class MentionAutocompletePresenter extends RecyclerViewPresenter implements FlexibleAdapter.OnItemClickListener { + private static final String TAG = "MentionAutocompletePresenter"; + + @Inject + NcApi ncApi; + + @Inject + UserManager userManager; + + @Inject + CurrentUserProviderNew currentUserProvider; + + @Inject + ViewThemeUtils viewThemeUtils; + + private User currentUser; + private FlexibleAdapter adapter; + private Context context; + + private String roomToken; + private int chatApiVersion; + + private List abstractFlexibleItemList = new ArrayList<>(); + + public MentionAutocompletePresenter(Context context) { + super(context); + this.context = context; + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + currentUser = currentUserProvider.getCurrentUser().blockingGet(); + } + + public MentionAutocompletePresenter(Context context, String roomToken, int chatApiVersion) { + super(context); + this.roomToken = roomToken; + this.context = context; + this.chatApiVersion = chatApiVersion; + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + currentUser = currentUserProvider.getCurrentUser().blockingGet(); + } + + @Override + protected RecyclerView.Adapter instantiateAdapter() { + adapter = new FlexibleAdapter<>(abstractFlexibleItemList, context, false); + adapter.addListener(this); + return adapter; + } + + @Override + protected PopupDimensions getPopupDimensions() { + PopupDimensions popupDimensions = new PopupDimensions(); + popupDimensions.width = ViewGroup.LayoutParams.MATCH_PARENT; + popupDimensions.height = ViewGroup.LayoutParams.WRAP_CONTENT; + return popupDimensions; + } + + @Override + protected void onQuery(@Nullable CharSequence query) { + + String queryString; + if (query != null && query.length() > 1) { + queryString = String.valueOf(query.subSequence(1, query.length())); + } else { + queryString = ""; + } + + adapter.setFilter(queryString); + + Map queryMap = new HashMap<>(); + queryMap.put("includeStatus", "true"); + + ncApi.getMentionAutocompleteSuggestions( + ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()), + ApiUtils.getUrlForMentionSuggestions(chatApiVersion, currentUser.getBaseUrl(), roomToken), + queryString, 5, queryMap) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retry(3) + .subscribe(new Observer() { + @Override + public void onSubscribe(@NonNull Disposable d) { + // no actions atm + } + + @Override + public void onNext(@NonNull MentionOverall mentionOverall) { + if (mentionOverall.getOcs() != null) { + List mentionsList = mentionOverall.getOcs().getData(); + + if (mentionsList != null) { + + if (mentionsList.isEmpty()) { + adapter.clear(); + } else { + List internalAbstractFlexibleItemList = + new ArrayList<>(mentionsList.size()); + for (Mention mention : mentionsList) { + internalAbstractFlexibleItemList.add( + new MentionAutocompleteItem( + mention, + currentUser, + context, + roomToken, + viewThemeUtils)); + } + + if (adapter.getItemCount() != 0) { + adapter.clear(); + } + + adapter.updateDataSet(internalAbstractFlexibleItemList); + } + } + } + } + + @SuppressLint("LongLogTag") + @Override + public void onError(@NonNull Throwable e) { + adapter.clear(); + Log.e(TAG, "failed to get MentionAutocompleteSuggestions", e); + } + + @Override + public void onComplete() { + // no actions atm + } + }); + } + + @Override + public boolean onItemClick(View view, int position) { + Mention mention = new Mention(); + MentionAutocompleteItem mentionAutocompleteItem = (MentionAutocompleteItem) adapter.getItem(position); + if (mentionAutocompleteItem != null) { + String mentionId = mentionAutocompleteItem.mentionId; + if (mentionId != null) { + mention.setMentionId(mentionId); + } + mention.setId(mentionAutocompleteItem.objectId); + mention.setLabel(mentionAutocompleteItem.displayName); + mention.setSource(mentionAutocompleteItem.source); + mention.setRoomToken(mentionAutocompleteItem.roomToken); + dispatchClick(mention); + } + return true; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt b/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt new file mode 100644 index 0000000..5786ca4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/profile/ProfileActivity.kt @@ -0,0 +1,780 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2021 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.profile + +import android.app.Activity +import android.content.pm.PackageManager +import android.os.Bundle +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toDrawable +import androidx.core.net.toFile +import androidx.core.net.toUri +import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.RecyclerView +import autodagger.AutoInjector +import com.github.dhaval2404.imagepicker.ImagePicker +import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityProfileBinding +import com.nextcloud.talk.databinding.UserInfoDetailsTableItemBinding +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.userprofile.Scope +import com.nextcloud.talk.models.json.userprofile.UserProfileData +import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall +import com.nextcloud.talk.models.json.userprofile.UserProfileOverall +import com.nextcloud.talk.ui.dialog.ScopeDialog +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.Mimetype.IMAGE_JPG +import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC +import com.nextcloud.talk.utils.PickImage +import com.nextcloud.talk.utils.PickImage.Companion.REQUEST_PERMISSION_CAMERA +import com.nextcloud.talk.utils.SpreedFeatures +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File +import java.util.LinkedList +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +@Suppress("Detekt.TooManyFunctions") +class ProfileActivity : BaseActivity() { + private lateinit var binding: ActivityProfileBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + private var currentUser: User? = null + private var edit = false + private var adapter: UserInfoAdapter? = null + private var userInfo: UserProfileData? = null + private var editableFields = ArrayList() + + private lateinit var pickImage: PickImage + + private val startImagePickerForResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + handleResult(it) { result -> + pickImage.onImagePickerResult(result.data) { uri -> + uploadAvatar(uri.toFile()) + } + } + } + + private val startSelectRemoteFilesIntentForResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + handleResult(it) { result -> + pickImage.onSelectRemoteFilesResult(startImagePickerForResult, result.data) + } + } + + private val startTakePictureIntentForResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + handleResult(it) { result -> + pickImage.onTakePictureResult(startImagePickerForResult, result.data) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + binding = ActivityProfileBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + initSystemBars() + } + + override fun onResume() { + super.onResume() + + adapter = UserInfoAdapter(null, viewThemeUtils, this) + binding.userinfoList.adapter = adapter + binding.userinfoList.setItemViewCacheSize(DEFAULT_CACHE_SIZE) + currentUser = currentUserProvider.currentUser.blockingGet() + val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) + + pickImage = PickImage(this, currentUser) + binding.avatarUpload.setOnClickListener { + pickImage.selectLocal(startImagePickerForResult = startImagePickerForResult) + } + binding.avatarChoose.setOnClickListener { + pickImage.selectRemote(startSelectRemoteFilesIntentForResult = startSelectRemoteFilesIntentForResult) + } + binding.avatarCamera.setOnClickListener { + pickImage.takePicture(startTakePictureIntentForResult = startTakePictureIntentForResult) + } + binding.avatarDelete.setOnClickListener { + ncApi.deleteAvatar( + credentials, + ApiUtils.getUrlForTempAvatar(currentUser!!.baseUrl!!) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + DisplayUtils.loadAvatarImage( + currentUser, + binding.avatarImage, + true + ) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to delete avatar", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + binding.avatarImage.let { ViewCompat.setTransitionName(it, "userAvatar.transitionTag") } + ncApi.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl!!)) + .retry(DEFAULT_RETRIES) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(userProfileOverall: UserProfileOverall) { + userInfo = userProfileOverall.ocs!!.data + showUserProfile() + } + + override fun onError(e: Throwable) { + setErrorMessageForMultiList( + getString(R.string.userinfo_no_info_headline), + getString(R.string.userinfo_error_text), + R.drawable.ic_list_empty_error + ) + } + + override fun onComplete() { + // unused atm + } + }) + + colorIcons() + } + + private fun setupActionBar() { + setSupportActionBar(binding.profileToolbar) + binding.profileToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(android.R.color.transparent, null).toDrawable()) + supportActionBar?.title = context.getString(R.string.nc_profile_personal_info_title) + viewThemeUtils.material.themeToolbar(binding.profileToolbar) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.menu_profile, menu) + return true + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + super.onPrepareOptionsMenu(menu) + menu.findItem(R.id.edit).isVisible = editableFields.size > 0 + if (edit) { + menu.findItem(R.id.edit).setTitle(R.string.save) + menu.findItem(R.id.edit).icon = ContextCompat.getDrawable(this, R.drawable.ic_check) + } else { + menu.findItem(R.id.edit).setTitle(R.string.edit) + menu.findItem(R.id.edit).icon = ContextCompat.getDrawable(this, R.drawable.ic_edit) + } + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.edit) { + if (edit) { + save() + } + edit = !edit + if (edit) { + item.setTitle(R.string.save) + item.icon = ContextCompat.getDrawable(this, R.drawable.ic_check) + binding.emptyList.root.visibility = View.GONE + binding.userinfoList.visibility = View.VISIBLE + if (CapabilitiesUtil.hasSpreedFeatureCapability( + currentUser!!.capabilities!!.spreedCapability!!, + SpreedFeatures.TEMP_USER_AVATAR_API + ) + ) { + // TODO later avatar can also be checked via user fields, for now it is in Talk capability + binding.avatarButtons.visibility = View.VISIBLE + } + ncApi.getEditableUserProfileFields( + ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token), + ApiUtils.getUrlForUserFields(currentUser!!.baseUrl!!) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(userProfileFieldsOverall: UserProfileFieldsOverall) { + editableFields = userProfileFieldsOverall.ocs!!.data!! + adapter!!.notifyDataSetChanged() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error loading editable user profile from server", e) + edit = false + } + + override fun onComplete() { + // unused atm + } + }) + } else { + item.setTitle(R.string.edit) + item.icon = ContextCompat.getDrawable(this, R.drawable.ic_edit) + + binding.avatarButtons.visibility = View.INVISIBLE + if (adapter!!.filteredDisplayList.isEmpty()) { + binding.emptyList.root.visibility = View.VISIBLE + binding.userinfoList.visibility = View.GONE + } + } + adapter!!.notifyDataSetChanged() + return true + } + return super.onOptionsItemSelected(item) + } + + private fun colorIcons() { + binding.let { + viewThemeUtils.material.themeFAB(it.avatarChoose) + viewThemeUtils.material.themeFAB(it.avatarCamera) + viewThemeUtils.material.themeFAB(it.avatarUpload) + viewThemeUtils.material.themeFAB(it.avatarDelete) + } + } + + private fun isAllEmpty(items: Array): Boolean { + for (item in items) { + if (!TextUtils.isEmpty(item)) { + return false + } + } + + return true + } + + private fun showUserProfile() { + if (currentUser!!.baseUrl != null) { + binding.userinfoBaseurl.text = currentUser!!.baseUrl!!.toUri().host + } + DisplayUtils.loadAvatarImage(currentUser, binding.avatarImage, false) + if (!TextUtils.isEmpty(userInfo?.displayName)) { + binding.userinfoFullName.text = userInfo?.displayName + } + binding.loadingContent.visibility = View.VISIBLE + adapter!!.setData(createUserInfoDetails(userInfo)) + if ( + isAllEmpty( + arrayOf( + userInfo?.displayName, + userInfo?.phone, + userInfo?.email, + userInfo?.address, + userInfo?.twitter, + userInfo?.website + ) + ) + ) { + binding.userinfoList.visibility = View.GONE + binding.loadingContent.visibility = View.GONE + binding.emptyList.root.visibility = View.VISIBLE + setErrorMessageForMultiList( + getString(R.string.userinfo_no_info_headline), + getString(R.string.userinfo_no_info_text), + R.drawable.ic_user + ) + } else { + binding.emptyList.root.visibility = View.GONE + binding.loadingContent.visibility = View.GONE + binding.userinfoList.visibility = View.VISIBLE + } + + // show edit button + if (CapabilitiesUtil.canEditScopes(currentUser!!)) { + ncApi.getEditableUserProfileFields( + ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token), + ApiUtils.getUrlForUserFields(currentUser!!.baseUrl!!) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(userProfileFieldsOverall: UserProfileFieldsOverall) { + editableFields = userProfileFieldsOverall.ocs!!.data!! + invalidateOptionsMenu() + adapter!!.notifyDataSetChanged() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error loading editable user profile from server", e) + edit = false + } + + override fun onComplete() { + // unused atm + } + }) + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun setErrorMessageForMultiList(headline: String, message: String, @DrawableRes errorResource: Int) { + binding.emptyList.emptyListViewHeadline.text = headline + binding.emptyList.emptyListViewText.text = message + binding.emptyList.emptyListIcon.setImageResource(errorResource) + binding.emptyList.emptyListIcon.visibility = View.VISIBLE + binding.emptyList.emptyListViewText.visibility = View.VISIBLE + binding.userinfoList.visibility = View.GONE + binding.loadingContent.visibility = View.GONE + } + + @Suppress("Detekt.LongMethod") + private fun createUserInfoDetails(userInfo: UserProfileData?): List { + val result: MutableList = LinkedList() + + if (userInfo != null) { + result.add( + UserInfoDetailsItem( + R.drawable.ic_user, + userInfo.displayName, + resources!!.getString(R.string.user_info_displayname), + Field.DISPLAYNAME, + userInfo.displayNameScope + ) + ) + result.add( + UserInfoDetailsItem( + R.drawable.ic_phone, + userInfo.phone, + resources!!.getString(R.string.user_info_phone), + Field.PHONE, + userInfo.phoneScope + ) + ) + result.add( + UserInfoDetailsItem( + R.drawable.ic_email, + userInfo.email, + resources!!.getString(R.string.user_info_email), + Field.EMAIL, + userInfo.emailScope + ) + ) + result.add( + UserInfoDetailsItem( + R.drawable.ic_map_marker, + userInfo.address, + resources!!.getString(R.string.user_info_address), + Field.ADDRESS, + userInfo.addressScope + ) + ) + result.add( + UserInfoDetailsItem( + R.drawable.ic_web, + DisplayUtils.beautifyURL(userInfo.website), + resources!!.getString(R.string.user_info_website), + Field.WEBSITE, + userInfo.websiteScope + ) + ) + result.add( + UserInfoDetailsItem( + R.drawable.ic_twitter, + DisplayUtils.beautifyTwitterHandle(userInfo.twitter), + resources!!.getString(R.string.user_info_twitter), + Field.TWITTER, + userInfo.twitterScope + ) + ) + } + return result + } + + private fun save() { + for (item in adapter!!.displayList!!) { + // Text + if (item.text != userInfo?.getValueByField(item.field)) { + val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) + ncApi.setUserData( + credentials, + ApiUtils.getUrlForUserData(currentUser!!.baseUrl!!, currentUser!!.userId!!), + item.field.fieldName, + item.text + ) + .retry(DEFAULT_RETRIES) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(userProfileOverall: GenericOverall) { + Log.d(TAG, "Successfully saved: " + item.text + " as " + item.field) + if (item.field == Field.DISPLAYNAME) { + binding?.userinfoFullName?.text = item.text + } + } + + override fun onError(e: Throwable) { + item.text = userInfo?.getValueByField(item.field) + Snackbar.make( + binding.root, + String.format( + resources!!.getString(R.string.failed_to_save), + item.field + ), + Snackbar.LENGTH_LONG + ).show() + adapter!!.updateFilteredList() + adapter!!.notifyDataSetChanged() + Log.e(TAG, "Failed to saved: " + item.text + " as " + item.field, e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + // Scope + if (item.scope != userInfo?.getScopeByField(item.field)) { + saveScope(item, userInfo) + } + adapter!!.updateFilteredList() + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == REQUEST_PERMISSION_CAMERA) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + pickImage.takePicture(startTakePictureIntentForResult = startTakePictureIntentForResult) + } else { + Snackbar + .make(binding.root, context.getString(R.string.take_photo_permission), Snackbar.LENGTH_LONG) + .show() + } + } + } + + private fun handleResult(result: ActivityResult, onResult: (result: ActivityResult) -> Unit) { + when (result.resultCode) { + Activity.RESULT_OK -> onResult(result) + + ImagePicker.RESULT_ERROR -> { + Snackbar.make(binding.root, getError(result.data), Snackbar.LENGTH_SHORT).show() + } + + else -> { + Log.i(TAG, "Task Cancelled") + } + } + } + + private fun uploadAvatar(file: File?) { + val builder = MultipartBody.Builder() + builder.setType(MultipartBody.FORM) + builder.addFormDataPart( + "files[]", + file!!.name, + file.asRequestBody(IMAGE_PREFIX_GENERIC.toMediaTypeOrNull()) + ) + val filePart: MultipartBody.Part = MultipartBody.Part.createFormData( + "files[]", + file.name, + file.asRequestBody(IMAGE_JPG.toMediaTypeOrNull()) + ) + + // upload file + ncApi.uploadAvatar( + ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token), + ApiUtils.getUrlForTempAvatar(currentUser!!.baseUrl!!), + filePart + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + DisplayUtils.loadAvatarImage(currentUser, binding?.avatarImage, true) + } + + override fun onError(e: Throwable) { + Snackbar.make( + binding.root, + context.getString(R.string.default_error_msg), + Snackbar + .LENGTH_LONG + ).show() + Log.e(TAG, "Error uploading avatar", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + fun saveScope(item: UserInfoDetailsItem, userInfo: UserProfileData?) { + val credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) + ncApi.setUserData( + credentials, + ApiUtils.getUrlForUserData(currentUser!!.baseUrl!!, currentUser!!.userId!!), + item.field.scopeName, + item.scope!!.name + ) + .retry(DEFAULT_RETRIES) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(userProfileOverall: GenericOverall) { + Log.d(TAG, "Successfully saved: " + item.scope + " as " + item.field) + } + + override fun onError(e: Throwable) { + item.scope = userInfo?.getScopeByField(item.field) + Log.e(TAG, "Failed to saved: " + item.scope + " as " + item.field, e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + class UserInfoDetailsItem( + @field:DrawableRes @param:DrawableRes + var icon: Int, + var text: String?, + var hint: String, + val field: Field, + var scope: Scope? + ) + + class UserInfoAdapter( + displayList: List?, + private val viewThemeUtils: ViewThemeUtils, + private val profileActivity: ProfileActivity + ) : RecyclerView.Adapter() { + var displayList: List? + var filteredDisplayList: MutableList = LinkedList() + + class ViewHolder(val binding: UserInfoDetailsTableItemBinding) : RecyclerView.ViewHolder(binding.root) + + init { + this.displayList = displayList ?: LinkedList() + } + + fun setData(displayList: List) { + this.displayList = displayList + updateFilteredList() + notifyDataSetChanged() + } + + fun updateFilteredList() { + filteredDisplayList.clear() + if (displayList != null) { + for (item in displayList!!) { + if (!TextUtils.isEmpty(item.text)) { + filteredDisplayList.add(item) + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val itemBinding = + UserInfoDetailsTableItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(itemBinding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item: UserInfoDetailsItem = if (profileActivity.edit) { + displayList!![position] + } else { + filteredDisplayList[position] + } + + initScopeElements(item, holder) + + holder.binding.icon.setImageResource(item.icon) + initUserInfoEditText(holder, item) + + holder.binding.icon.contentDescription = item.hint + viewThemeUtils.platform.colorImageView(holder.binding.icon, ColorRole.PRIMARY) + if (!TextUtils.isEmpty(item.text) || profileActivity.edit) { + holder.binding.userInfoDetailContainer.visibility = View.VISIBLE + profileActivity.viewThemeUtils.material.colorTextInputLayout(holder.binding.userInfoInputLayout) + if (profileActivity.edit && + profileActivity.editableFields.contains(item.field.toString().lowercase()) + ) { + holder.binding.userInfoEditTextEdit.isEnabled = true + holder.binding.userInfoEditTextEdit.isFocusableInTouchMode = true + holder.binding.userInfoEditTextEdit.isEnabled = true + holder.binding.userInfoEditTextEdit.isCursorVisible = true + holder.binding.scope.setOnClickListener { + ScopeDialog( + holder.binding.scope.context, + this, + item.field, + holder.adapterPosition + ).show() + } + holder.binding.scope.alpha = HIGH_EMPHASIS_ALPHA + } else { + holder.binding.userInfoEditTextEdit.isEnabled = false + holder.binding.userInfoEditTextEdit.isFocusableInTouchMode = false + holder.binding.userInfoEditTextEdit.isEnabled = false + holder.binding.userInfoEditTextEdit.isCursorVisible = false + holder.binding.scope.setOnClickListener(null) + holder.binding.scope.alpha = MEDIUM_EMPHASIS_ALPHA + } + } else { + holder.binding.userInfoDetailContainer.visibility = View.GONE + } + } + + private fun initUserInfoEditText(holder: ViewHolder, item: UserInfoDetailsItem) { + holder.binding.userInfoEditTextEdit.setText(item.text) + holder.binding.userInfoInputLayout.hint = item.hint + holder.binding.userInfoEditTextEdit.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // unused atm + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + if (profileActivity.edit) { + displayList!![holder.adapterPosition].text = holder.binding.userInfoEditTextEdit.text.toString() + } else { + filteredDisplayList[holder.adapterPosition].text = + holder.binding.userInfoEditTextEdit.text.toString() + } + } + + override fun afterTextChanged(s: Editable) { + // unused atm + } + }) + } + + private fun initScopeElements(item: UserInfoDetailsItem, holder: ViewHolder) { + if (item.scope == null) { + holder.binding.scope.visibility = View.GONE + } else { + holder.binding.scope.visibility = View.VISIBLE + when (item.scope) { + Scope.PRIVATE -> holder.binding.scope.setImageResource(R.drawable.ic_cellphone) + Scope.LOCAL -> holder.binding.scope.setImageResource(R.drawable.ic_password) + Scope.FEDERATED -> holder.binding.scope.setImageResource(R.drawable.ic_contacts) + Scope.PUBLISHED -> holder.binding.scope.setImageResource(R.drawable.ic_link) + null -> { + // nothing + } + } + holder.binding.scope.contentDescription = holder.binding.scope.context.getString( + R.string.scope_toggle_description, + item.hint + ) + } + } + + override fun getItemCount(): Int = + if (profileActivity.edit) { + displayList!!.size + } else { + filteredDisplayList.size + } + + fun updateScope(position: Int, scope: Scope?) { + displayList!![position].scope = scope + notifyDataSetChanged() + } + } + + enum class Field(val fieldName: String, val scopeName: String) { + EMAIL("email", "emailScope"), + DISPLAYNAME("displayname", "displaynameScope"), + PHONE("phone", "phoneScope"), + ADDRESS("address", "addressScope"), + WEBSITE("website", "websiteScope"), + TWITTER("twitter", "twitterScope") + } + + companion object { + private val TAG = ProfileActivity::class.java.simpleName + private const val DEFAULT_CACHE_SIZE: Int = 20 + private const val DEFAULT_RETRIES: Long = 3 + private const val HIGH_EMPHASIS_ALPHA: Float = 0.87f + private const val MEDIUM_EMPHASIS_ALPHA: Float = 0.6f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceModel.kt b/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceModel.kt new file mode 100644 index 0000000..d270bc6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceModel.kt @@ -0,0 +1,9 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.raisehand + +data class RequestAssistanceModel(var success: Boolean) diff --git a/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepository.kt b/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepository.kt new file mode 100644 index 0000000..f7cfcd2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepository.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.raisehand + +import io.reactivex.Observable + +interface RequestAssistanceRepository { + + fun requestAssistance(roomToken: String): Observable + + fun withdrawRequestAssistance(roomToken: String): Observable +} diff --git a/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepositoryImpl.kt new file mode 100644 index 0000000..3ee14f4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/raisehand/RequestAssistanceRepositoryImpl.kt @@ -0,0 +1,61 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.raisehand + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.generic.GenericMeta +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable + +class RequestAssistanceRepositoryImpl(private val ncApi: NcApi, currentUserProvider: CurrentUserProviderNew) : + RequestAssistanceRepository { + + val currentUser: User = currentUserProvider.currentUser.blockingGet() + val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!! + + var apiVersion = 1 + + override fun requestAssistance(roomToken: String): Observable = + ncApi.requestAssistance( + credentials, + ApiUtils.getUrlForRequestAssistance( + apiVersion, + currentUser.baseUrl, + roomToken + ) + ).map { mapToRequestAssistanceModel(it.ocs?.meta!!) } + + override fun withdrawRequestAssistance(roomToken: String): Observable = + ncApi.withdrawRequestAssistance( + credentials, + ApiUtils.getUrlForRequestAssistance( + apiVersion, + currentUser.baseUrl, + roomToken + ) + ).map { mapToWithdrawRequestAssistanceModel(it.ocs?.meta!!) } + + private fun mapToRequestAssistanceModel(response: GenericMeta): RequestAssistanceModel { + val success = response.statusCode == HTTP_OK + return RequestAssistanceModel( + success + ) + } + + private fun mapToWithdrawRequestAssistanceModel(response: GenericMeta): WithdrawRequestAssistanceModel { + val success = response.statusCode == HTTP_OK + return WithdrawRequestAssistanceModel( + success + ) + } + + companion object { + private const val HTTP_OK: Int = 200 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/raisehand/WithdrawRequestAssistanceModel.kt b/app/src/main/java/com/nextcloud/talk/raisehand/WithdrawRequestAssistanceModel.kt new file mode 100644 index 0000000..f84c0cc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/raisehand/WithdrawRequestAssistanceModel.kt @@ -0,0 +1,9 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.raisehand + +data class WithdrawRequestAssistanceModel(var success: Boolean) diff --git a/app/src/main/java/com/nextcloud/talk/raisehand/viewmodel/RaiseHandViewModel.kt b/app/src/main/java/com/nextcloud/talk/raisehand/viewmodel/RaiseHandViewModel.kt new file mode 100644 index 0000000..12d609d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/raisehand/viewmodel/RaiseHandViewModel.kt @@ -0,0 +1,121 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.raisehand.viewmodel + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.raisehand.RequestAssistanceModel +import com.nextcloud.talk.raisehand.RequestAssistanceRepository +import com.nextcloud.talk.raisehand.WithdrawRequestAssistanceModel +import com.nextcloud.talk.users.UserManager +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class RaiseHandViewModel @Inject constructor(private val repository: RequestAssistanceRepository) : ViewModel() { + + @Inject + lateinit var userManager: UserManager + + lateinit var roomToken: String + private var isBreakoutRoom: Boolean = false + + sealed interface ViewState + + object RaisedHandState : ViewState + object LoweredHandState : ViewState + object ErrorState : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(LoweredHandState) + val viewState: LiveData + get() = _viewState + + fun clickHandButton() { + when (viewState.value) { + LoweredHandState -> { + raiseHand() + } + RaisedHandState -> { + lowerHand() + } + else -> {} + } + } + + private fun raiseHand() { + _viewState.value = RaisedHandState + if (isBreakoutRoom) { + repository.requestAssistance(roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(RequestAssistanceObserver()) + } + } + + fun lowerHand() { + _viewState.value = LoweredHandState + if (isBreakoutRoom) { + repository.withdrawRequestAssistance(roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(WithdrawRequestAssistanceObserver()) + } + } + + fun setData(roomToken: String, isBreakoutRoom: Boolean) { + this.roomToken = roomToken + this.isBreakoutRoom = isBreakoutRoom + } + + inner class RequestAssistanceObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(requestAssistanceModel: RequestAssistanceModel) { + // RaisedHandState was already set because it's also used for signaling message + Log.d(TAG, "requestAssistance successful") + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failure in RequestAssistanceObserver", e) + _viewState.value = ErrorState + } + + override fun onComplete() { + // dismiss() + } + } + + inner class WithdrawRequestAssistanceObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(withdrawRequestAssistanceModel: WithdrawRequestAssistanceModel) { + // LoweredHandState was already set because it's also used for signaling message + Log.d(TAG, "withdrawRequestAssistance successful") + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failure in WithdrawRequestAssistanceObserver", e) + _viewState.value = ErrorState + } + + override fun onComplete() { + // dismiss() + } + } + + companion object { + private val TAG = RaiseHandViewModel::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt new file mode 100644 index 0000000..8612dbe --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/receivers/DirectReplyReceiver.kt @@ -0,0 +1,186 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Dariusz Olszewski + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.receivers + +import android.Manifest +import android.app.Notification +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.text.SpannableStringBuilder +import android.text.style.ForegroundColorSpan +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.Person +import androidx.core.app.RemoteInput +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.message.SendMessageUtils +import io.reactivex.Observer +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class DirectReplyReceiver : BroadcastReceiver() { + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var ncApi: NcApi + + lateinit var context: Context + lateinit var currentUser: User + private var systemNotificationId: Int? = null + private var roomToken: String? = null + private var replyMessage: CharSequence? = null + + init { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + } + + override fun onReceive(receiveContext: Context, intent: Intent?) { + context = receiveContext + + // NOTE - systemNotificationId is an internal ID used on the device only. + // It is NOT the same as the notification ID used in communication with the server. + systemNotificationId = intent!!.getIntExtra(KEY_SYSTEM_NOTIFICATION_ID, 0) + roomToken = intent.getStringExtra(KEY_ROOM_TOKEN) + + val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!) + currentUser = userManager.getUserWithId(id).blockingGet() + + replyMessage = getMessageText(intent) + sendDirectReply() + } + + private fun getMessageText(intent: Intent): CharSequence? = + RemoteInput.getResultsFromIntent(intent)?.getCharSequence(NotificationUtils.KEY_DIRECT_REPLY) + + private fun sendDirectReply() { + val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) + val apiVersion = ApiUtils.getChatApiVersion(currentUser.capabilities!!.spreedCapability!!, intArrayOf(1)) + val url = ApiUtils.getUrlForChat(apiVersion, currentUser.baseUrl!!, roomToken!!) + + ncApi.sendChatMessage( + credentials, + url, + replyMessage, + currentUser.displayName, + null, + false, + SendMessageUtils().generateReferenceId() + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(message: ChatOverallSingleMessage) { + confirmReplySent() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to send reply", e) + informReplyFailed() + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun confirmReplySent() { + appendMessageToNotification(replyMessage!!) + } + + private fun informReplyFailed() { + val errorColor = ForegroundColorSpan(context.resources.getColor(R.color.medium_emphasis_text, context.theme)) + val errorMessageHeader = context.resources.getString(R.string.nc_message_failed_to_send) + val errorMessage = SpannableStringBuilder().append("$errorMessageHeader\n$replyMessage", errorColor, 0) + appendMessageToNotification(errorMessage) + } + + private fun findActiveNotification(notificationId: Int): Notification? { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + return notificationManager.activeNotifications.find { it.id == notificationId }?.notification + } + + private fun appendMessageToNotification(reply: CharSequence) { + // Implementation inspired by the SO question and article below: + // https://stackoverflow.com/questions/51549456/android-o-notification-for-direct-reply-message + // https://medium.com/@sidorovroman3/android-how-to-use-messagingstyle-for-notifications-without-caching-messages-c414ef2b816c + // + // Tries to follow "Best practices for messaging apps" described here: + // https://developer.android.com/training/notify-user/build-notification#messaging-best-practices + + // Find the original (active) notification + val previousNotification = findActiveNotification(systemNotificationId!!) ?: return + + // Recreate builder based on the active notification + val previousBuilder = NotificationCompat.Builder(context, previousNotification) + + // Extract MessagingStyle from the active notification + val previousStyle = NotificationCompat.MessagingStyle + .extractMessagingStyleFromNotification(previousNotification) + + // Add reply + Single.fromCallable { + val avatarUrl = ApiUtils.getUrlForAvatar(currentUser.baseUrl!!, currentUser.userId, false) + val me = Person.Builder() + .setName(currentUser.displayName) + .setIcon(NotificationUtils.loadAvatarSync(avatarUrl, context)) + .build() + val message = NotificationCompat.MessagingStyle.Message(reply, System.currentTimeMillis(), me) + previousStyle?.addMessage(message) + + // Set the updated style + previousBuilder.setStyle(previousStyle) + + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + // Check if notification still exists + if (findActiveNotification(systemNotificationId!!) != null) { + NotificationManagerCompat.from(context).notify(systemNotificationId!!, previousBuilder.build()) + } + } + } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + companion object { + const val TAG = "DirectReplyReceiver" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt new file mode 100644 index 0000000..9de18c1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/receivers/DismissRecordingAvailableReceiver.kt @@ -0,0 +1,99 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022-2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.receivers + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import autodagger.AutoInjector +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class DismissRecordingAvailableReceiver : BroadcastReceiver() { + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var ncApi: NcApi + + lateinit var context: Context + lateinit var currentUser: User + private var systemNotificationId: Int? = null + private var link: String? = null + + init { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + } + + override fun onReceive(receiveContext: Context, intent: Intent?) { + context = receiveContext + + // NOTE - systemNotificationId is an internal ID used on the device only. + // It is NOT the same as the notification ID used in communication with the server. + systemNotificationId = intent!!.getIntExtra(KEY_SYSTEM_NOTIFICATION_ID, 0) + link = intent.getStringExtra(BundleKeys.KEY_DISMISS_RECORDING_URL) + + val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!) + currentUser = userManager.getUserWithId(id).blockingGet() + + dismissNcRecordingAvailableNotification() + } + + private fun dismissNcRecordingAvailableNotification() { + val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) + + ncApi.sendCommonDeleteRequest(credentials, link) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + cancelNotification(systemNotificationId!!) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to send dismiss for recording available", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun cancelNotification(notificationId: Int) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(notificationId) + } + + companion object { + private val TAG = DismissRecordingAvailableReceiver::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt new file mode 100644 index 0000000..6c9f22a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/receivers/MarkAsReadReceiver.kt @@ -0,0 +1,109 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Dariusz Olszewski + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.receivers + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import autodagger.AutoInjector +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MESSAGE_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class MarkAsReadReceiver : BroadcastReceiver() { + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var ncApi: NcApi + + lateinit var context: Context + lateinit var currentUser: User + private var systemNotificationId: Int? = null + private var roomToken: String? = null + private var messageId: Int = 0 + + init { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + } + + override fun onReceive(receiveContext: Context, intent: Intent?) { + context = receiveContext + + // NOTE - systemNotificationId is an internal ID used on the device only. + // It is NOT the same as the notification ID used in communication with the server. + systemNotificationId = intent!!.getIntExtra(KEY_SYSTEM_NOTIFICATION_ID, 0) + roomToken = intent.getStringExtra(KEY_ROOM_TOKEN) + messageId = intent.getIntExtra(KEY_MESSAGE_ID, 0) + + val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!) + currentUser = userManager.getUserWithId(id).blockingGet() + + markAsRead() + } + + private fun markAsRead() { + val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) + val apiVersion = ApiUtils.getChatApiVersion(currentUser.capabilities!!.spreedCapability!!, intArrayOf(1)) + val url = ApiUtils.getUrlForChatReadMarker( + apiVersion, + currentUser.baseUrl!!, + roomToken!! + ) + + ncApi.setChatReadMarker(credentials, url, messageId) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + cancelNotification(systemNotificationId!!) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to set chat read marker", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun cancelNotification(notificationId: Int) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(notificationId) + } + + companion object { + const val TAG = "MarkAsReadReceiver" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt new file mode 100644 index 0000000..5ce9e1c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.nextcloud.talk.utils.NotificationUtils + +class PackageReplacedReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent?) { + if (intent != null && + intent.action != null && + intent.action == "android.intent.action.MY_PACKAGE_REPLACED" + ) { + NotificationUtils.removeOldNotificationChannels(context) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt new file mode 100644 index 0000000..883cdec --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/receivers/ShareRecordingToChatReceiver.kt @@ -0,0 +1,110 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022-2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.receivers + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import android.view.View +import autodagger.AutoInjector +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ShareRecordingToChatReceiver : BroadcastReceiver() { + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var ncApi: NcApi + + lateinit var context: Context + lateinit var currentUser: User + private var systemNotificationId: Int? = null + private var link: String? = null + + init { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + } + + override fun onReceive(receiveContext: Context, intent: Intent?) { + context = receiveContext + systemNotificationId = intent!!.getIntExtra(KEY_SYSTEM_NOTIFICATION_ID, 0) + link = intent.getStringExtra(BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL) + + val id = intent.getLongExtra(KEY_INTERNAL_USER_ID, currentUserProvider.currentUser.blockingGet().id!!) + currentUser = userManager.getUserWithId(id).blockingGet() + + shareRecordingToChat() + } + + private fun shareRecordingToChat() { + val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) + + ncApi.sendCommonPostRequest(credentials, link) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + cancelNotification(systemNotificationId!!) + + // Here it would make sense to open the chat where the recording was shared to (startActivity...). + // However, as we are in a broadcast receiver, this needs a TaskStackBuilder + // combined with addNextIntentWithParentStack. For further reading, see + // https://developer.android.com/develop/ui/views/notifications/navigation#DirectEntry + + Snackbar.make( + View(context), + context.resources.getString(R.string.nc_all_ok_operation), + Snackbar.LENGTH_LONG + ).show() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to share recording to chat request", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun cancelNotification(notificationId: Int) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(notificationId) + } + + companion object { + private val TAG = ShareRecordingToChatReceiver::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/SelectionInterface.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/SelectionInterface.kt new file mode 100644 index 0000000..8bcc9d4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/SelectionInterface.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.remotefilebrowser + +interface SelectionInterface { + fun isPathSelected(path: String): Boolean +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt new file mode 100644 index 0000000..93a0aec --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt @@ -0,0 +1,279 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.remotefilebrowser.activities + +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.ActivityRemoteFileBrowserBinding +import com.nextcloud.talk.remotefilebrowser.SelectionInterface +import com.nextcloud.talk.remotefilebrowser.adapters.RemoteFileBrowserItemsAdapter +import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel +import com.nextcloud.talk.ui.dialog.SortingOrderDialogFragment +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.FileSortOrder +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MIME_TYPE_FILTER +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class RemoteFileBrowserActivity : + AppCompatActivity(), + SelectionInterface, + SwipeRefreshLayout.OnRefreshListener { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var dateUtils: DateUtils + + private lateinit var binding: ActivityRemoteFileBrowserBinding + private lateinit var viewModel: RemoteFileBrowserItemsViewModel + + private var filesSelectionDoneMenuItem: MenuItem? = null + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + setResult(Activity.RESULT_CANCELED) + finish() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityRemoteFileBrowserBinding.inflate(layoutInflater) + setSupportActionBar(binding.remoteFileBrowserItemsToolbar) + viewThemeUtils.material.themeToolbar(binding.remoteFileBrowserItemsToolbar) + viewThemeUtils.talk.themeSortListButtonGroup(binding.sortListButtonGroup) + viewThemeUtils.talk.themeSortButton(binding.sortButton) + viewThemeUtils.material.colorMaterialTextButton(binding.sortButton) + viewThemeUtils.talk.themePathNavigationButton(binding.pathNavigationBackButton) + viewThemeUtils.material.colorMaterialTextButton(binding.pathNavigationBackButton) + viewThemeUtils.platform.themeStatusBar(this) + setContentView(binding.root) + initSystemBars() + + DisplayUtils.applyColorToNavigationBar( + this.window, + ResourcesCompat.getColor(resources, R.color.bg_default, null) + ) + + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + val extras = intent.extras + val mimeTypeSelectionFilter = extras?.getString(KEY_MIME_TYPE_FILTER, null) + + initViewModel(mimeTypeSelectionFilter) + + binding.swipeRefreshList.setOnRefreshListener(this) + viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeRefreshList) + + binding.pathNavigationBackButton.setOnClickListener { viewModel.navigateUp() } + binding.sortButton.setOnClickListener { changeSorting() } + + viewModel.loadItems() + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + private fun initViewModel(mimeTypeSelectionFilter: String?) { + viewModel = ViewModelProvider(this, viewModelFactory)[RemoteFileBrowserItemsViewModel::class.java] + + viewModel.viewState.observe(this) { state -> + clearEmptyLoading() + when (state) { + is RemoteFileBrowserItemsViewModel.LoadingItemsState, RemoteFileBrowserItemsViewModel.InitialState -> { + showLoading() + } + is RemoteFileBrowserItemsViewModel.NoRemoteFileItemsState -> { + showEmpty() + } + is RemoteFileBrowserItemsViewModel.LoadedState -> { + loadList(state, mimeTypeSelectionFilter) + } + is RemoteFileBrowserItemsViewModel.FinishState -> { + finishWithResult(state.selectedPaths) + } + + else -> {} + } + } + + viewModel.fileSortOrder.observe(this) { sortOrder -> + if (sortOrder != null) { + binding.sortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder)) + } + } + + viewModel.currentPath.observe(this) { path -> + if (path != null) { + supportActionBar?.title = path + } + } + + viewModel.selectedPaths.observe(this) { selectedPaths -> + filesSelectionDoneMenuItem?.isVisible = !selectedPaths.isNullOrEmpty() + } + } + + private fun loadList(state: RemoteFileBrowserItemsViewModel.LoadedState, mimeTypeSelectionFilter: String?) { + val remoteFileBrowserItems = state.items + Log.d(TAG, "Items received: $remoteFileBrowserItems") + + // TODO make showGrid based on preferences (when available) + val showGrid = false + val layoutManager = if (showGrid) { + GridLayoutManager(this, SPAN_COUNT) + } else { + LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + } + + // TODO do not needlessly recreate adapter if it can be reused + val adapter = RemoteFileBrowserItemsAdapter( + showGrid = showGrid, + mimeTypeSelectionFilter = mimeTypeSelectionFilter, + user = currentUserProvider.currentUser.blockingGet(), + selectionInterface = this, + viewThemeUtils = viewThemeUtils, + dateUtils = dateUtils, + onItemClicked = viewModel::onItemClicked + ) + adapter.items = remoteFileBrowserItems + + binding.recyclerView.adapter = adapter + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.setHasFixedSize(true) + + showList() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.menu_share_files, menu) + filesSelectionDoneMenuItem = menu.findItem(R.id.files_selection_done) + return true + } + + override fun onResume() { + super.onResume() + refreshCurrentPath() + } + + private fun changeSorting() { + val newFragment: DialogFragment = SortingOrderDialogFragment + .newInstance(FileSortOrder.getFileSortOrder(viewModel.fileSortOrder.value!!.name)) + newFragment.show( + supportFragmentManager, + SortingOrderDialogFragment.SORTING_ORDER_FRAGMENT + ) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressedDispatcher.onBackPressed() + true + } + R.id.files_selection_done -> { + viewModel.onSelectionDone() + true + } + else -> { + return super.onOptionsItemSelected(item) + } + } + } + + private fun finishWithResult(selectedPaths: Set) { + val data = Intent() + data.putStringArrayListExtra(EXTRA_SELECTED_PATHS, ArrayList(selectedPaths)) + setResult(Activity.RESULT_OK, data) + finish() + } + + private fun clearEmptyLoading() { + binding.emptyContainer.emptyListView.visibility = View.GONE + } + + private fun showLoading() { + binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.file_list_loading) + binding.emptyContainer.emptyListView.visibility = View.VISIBLE + binding.recyclerView.visibility = View.GONE + } + + private fun showEmpty() { + binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.nc_shared_items_empty) + binding.emptyContainer.emptyListView.visibility = View.VISIBLE + binding.recyclerView.visibility = View.GONE + } + + private fun showList() { + binding.recyclerView.visibility = View.VISIBLE + } + + fun initSystemBars() { + val decorView = window.decorView + decorView.setOnApplyWindowInsetsListener { view, insets -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + } + insets + } + ViewCompat.requestApplyInsets(decorView) + } + + override fun onRefresh() { + refreshCurrentPath() + } + + private fun refreshCurrentPath() { + viewModel.loadItems() + } + + override fun isPathSelected(path: String): Boolean = viewModel.isPathSelected(path) + + companion object { + private val TAG = RemoteFileBrowserActivity::class.simpleName + const val SPAN_COUNT: Int = 4 + const val EXTRA_SELECTED_PATHS = "EXTRA_SELECTED_PATH" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt new file mode 100644 index 0000000..767ee8b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt @@ -0,0 +1,76 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.remotefilebrowser.adapters + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.RvItemBrowserFileBinding +import com.nextcloud.talk.remotefilebrowser.SelectionInterface +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.DateUtils + +class RemoteFileBrowserItemsAdapter( + private val showGrid: Boolean = false, + private val mimeTypeSelectionFilter: String? = null, + private val user: User, + private val selectionInterface: SelectionInterface, + private val viewThemeUtils: ViewThemeUtils, + private val dateUtils: DateUtils, + private val onItemClicked: (RemoteFileBrowserItem) -> Unit +) : RecyclerView.Adapter() { + + var items: List = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RemoteFileBrowserItemsViewHolder = + if (showGrid) { + RemoteFileBrowserItemsListViewHolder( + RvItemBrowserFileBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + mimeTypeSelectionFilter, + user, + selectionInterface, + viewThemeUtils, + dateUtils + ) { + onItemClicked(items[it]) + } + } else { + RemoteFileBrowserItemsListViewHolder( + RvItemBrowserFileBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + mimeTypeSelectionFilter, + user, + selectionInterface, + viewThemeUtils, + dateUtils + ) { + onItemClicked(items[it]) + } + } + + override fun onBindViewHolder(holder: RemoteFileBrowserItemsViewHolder, position: Int) { + holder.onBind(items[position]) + } + + override fun getItemCount(): Int = items.size + + @SuppressLint("NotifyDataSetChanged") + fun updateDataSet(browserItems: List) { + items = browserItems + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt new file mode 100644 index 0000000..9205979 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt @@ -0,0 +1,127 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.remotefilebrowser.adapters + +import android.text.format.Formatter +import android.view.View +import android.widget.ImageView +import com.nextcloud.talk.R +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.RvItemBrowserFileBinding +import com.nextcloud.talk.extensions.loadImage +import com.nextcloud.talk.remotefilebrowser.SelectionInterface +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.Mimetype.FOLDER + +class RemoteFileBrowserItemsListViewHolder( + override val binding: RvItemBrowserFileBinding, + mimeTypeSelectionFilter: String?, + currentUser: User, + selectionInterface: SelectionInterface, + private val viewThemeUtils: ViewThemeUtils, + private val dateUtils: DateUtils, + onItemClicked: (Int) -> Unit +) : RemoteFileBrowserItemsViewHolder(binding, mimeTypeSelectionFilter, currentUser, selectionInterface) { + + override val fileIcon: ImageView + get() = binding.fileIcon + + private var selectable: Boolean = true + private var clickable: Boolean = true + + init { + itemView.setOnClickListener { + if (clickable) { + onItemClicked(bindingAdapterPosition) + if (selectable) { + binding.selectFileCheckbox.toggle() + } + } + } + } + + override fun onBind(item: RemoteFileBrowserItem) { + super.onBind(item) + + if (!item.isAllowedToReShare || item.isEncrypted) { + binding.root.isEnabled = false + binding.root.alpha = DISABLED_ALPHA + } else { + binding.root.isEnabled = true + binding.root.alpha = ENABLED_ALPHA + } + + binding.fileEncryptedImageView.visibility = + if (item.isEncrypted) { + View.VISIBLE + } else { + View.GONE + } + + binding.fileFavoriteImageView.visibility = + if (item.isFavorite) { + View.VISIBLE + } else { + View.GONE + } + + calculateSelectability(item) + calculateClickability(item, selectable) + setSelectability() + + val placeholder = viewThemeUtils.talk.getPlaceholderImage(binding.root.context, item.mimeType) + + if (item.hasPreview) { + val path = ApiUtils.getUrlForFilePreviewWithRemotePath( + currentUser.baseUrl!!, + item.path, + fileIcon.context.resources.getDimensionPixelSize(R.dimen.small_item_height) + ) + if (path.isNotEmpty()) { + fileIcon.loadImage(path, currentUser, placeholder) + } + } else { + fileIcon.setImageDrawable(placeholder) + } + + binding.filenameTextView.text = item.displayName + binding.fileModifiedInfo.text = String.format( + binding.fileModifiedInfo.context.getString(R.string.nc_last_modified), + Formatter.formatShortFileSize(binding.fileModifiedInfo.context, item.size), + dateUtils.getLocalDateTimeStringFromTimestamp(item.modifiedTimestamp) + ) + + binding.selectFileCheckbox.isChecked = selectionInterface.isPathSelected(item.path!!) + } + + private fun setSelectability() { + if (selectable) { + binding.selectFileCheckbox.visibility = View.VISIBLE + viewThemeUtils.platform.themeCheckbox(binding.selectFileCheckbox) + } else { + binding.selectFileCheckbox.visibility = View.GONE + } + } + + private fun calculateSelectability(item: RemoteFileBrowserItem) { + selectable = item.isFile && + (mimeTypeSelectionFilter == null || item.mimeType?.startsWith(mimeTypeSelectionFilter) == true) && + (item.isAllowedToReShare && !item.isEncrypted) + } + + private fun calculateClickability(item: RemoteFileBrowserItem, selectableItem: Boolean) { + clickable = selectableItem || FOLDER == item.mimeType + } + + companion object { + private const val DISABLED_ALPHA: Float = 0.38f + private const val ENABLED_ALPHA: Float = 1.0f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt new file mode 100644 index 0000000..02da0c7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.remotefilebrowser.adapters + +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.remotefilebrowser.SelectionInterface +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.utils.DrawableUtils + +abstract class RemoteFileBrowserItemsViewHolder( + open val binding: ViewBinding, + val mimeTypeSelectionFilter: String? = null, + val currentUser: User, + val selectionInterface: SelectionInterface +) : RecyclerView.ViewHolder(binding.root) { + + abstract val fileIcon: ImageView + + open fun onBind(item: RemoteFileBrowserItem) { + fileIcon.setImageResource(DrawableUtils.getDrawableResourceIdForMimeType(item.mimeType)) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/model/RemoteFileBrowserItem.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/model/RemoteFileBrowserItem.kt new file mode 100644 index 0000000..2206c67 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/model/RemoteFileBrowserItem.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.remotefilebrowser.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class RemoteFileBrowserItem( + var path: String? = null, + var displayName: String? = null, + var mimeType: String? = null, + var modifiedTimestamp: Long = 0, + var size: Long = 0, + var isFile: Boolean = false, + + // Used for remote files + var remoteId: String? = null, + var hasPreview: Boolean = false, + var isFavorite: Boolean = false, + var isEncrypted: Boolean = false, + var permissions: String? = null, + var isAllowedToReShare: Boolean = false +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt new file mode 100644 index 0000000..30a6515 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.remotefilebrowser.repositories + +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import io.reactivex.Observable + +interface RemoteFileBrowserItemsRepository { + + fun listFolder(path: String): Observable> +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt new file mode 100644 index 0000000..ba01f2d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.remotefilebrowser.repositories + +import com.nextcloud.talk.filebrowser.webdav.ReadFolderListingOperation +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable +import okhttp3.OkHttpClient +import javax.inject.Inject + +class RemoteFileBrowserItemsRepositoryImpl @Inject constructor( + private val okHttpClient: OkHttpClient, + private val userProvider: CurrentUserProviderNew +) : RemoteFileBrowserItemsRepository { + + private val user: User + get() = userProvider.currentUser.blockingGet() + + override fun listFolder(path: String): Observable> { + return Observable.fromCallable { + val operation = + ReadFolderListingOperation( + okHttpClient, + user, + path, + 1 + ) + val davResponse = operation.readRemotePath() + if (davResponse.getData() != null) { + return@fromCallable davResponse.getData() as List + } + return@fromCallable emptyList() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt new file mode 100644 index 0000000..c7d53ed --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt @@ -0,0 +1,221 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.remotefilebrowser.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.R +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepository +import com.nextcloud.talk.utils.FileSortOrder +import com.nextcloud.talk.utils.Mimetype.FOLDER +import com.nextcloud.talk.utils.preferences.AppPreferencesImpl +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import java.io.File +import javax.inject.Inject + +/** + * @startuml + * hide empty description + * [*] --> InitialState + * InitialState --> LoadingItemsState + * LoadingItemsState --> NoRemoteFileItemsState + * NoRemoteFileItemsState --> LoadingItemsState + * LoadingItemsState --> LoadedState + * LoadedState --> LoadingItemsState + * LoadedState --> FinishState + * FinishState --> [*] + * @enduml + */ +@OptIn(ExperimentalCoroutinesApi::class) +class RemoteFileBrowserItemsViewModel +@Inject +constructor( + private val repository: RemoteFileBrowserItemsRepository, + private val appPreferences: AppPreferencesImpl +) : ViewModel() { + + sealed interface ViewState + object InitialState : ViewState + object NoRemoteFileItemsState : ViewState + object LoadingItemsState : ViewState + class LoadedState(val items: List) : ViewState + class FinishState(val selectedPaths: Set) : ViewState + + private val initialSortOrder = FileSortOrder.getFileSortOrder(appPreferences.sorting) + + private var sortingFlow: Flow + + private val _viewState: MutableLiveData = MutableLiveData(InitialState) + val viewState: LiveData + get() = _viewState + + // TODO incorporate into view state object? + private val _fileSortOrder: MutableLiveData = MutableLiveData(initialSortOrder) + val fileSortOrder: LiveData + get() = _fileSortOrder + + private val _currentPath: MutableLiveData = MutableLiveData(ROOT_PATH) + val currentPath: LiveData + get() = _currentPath + + private val _selectedPaths: MutableLiveData> = MutableLiveData(emptySet()) + val selectedPaths: LiveData> + get() = _selectedPaths + + init { + val key = appPreferences.context.resources.getString(R.string.nc_file_browser_sort_by_key) + sortingFlow = appPreferences.readString(key) + CoroutineScope(Dispatchers.Main).launch { + var state = appPreferences.sorting + sortingFlow.collect { newString -> + if (newString != state) { + state = newString + onSelectSortOrder(newString) + } + } + } + } + + fun loadItems() { + _viewState.value = LoadingItemsState + repository.listFolder(currentPath.value!!).subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(RemoteFileBrowserItemsObserver()) + } + + inner class RemoteFileBrowserItemsObserver : Observer> { + + var newRemoteFileBrowserItems: List? = null + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(response: List) { + newRemoteFileBrowserItems = fileSortOrder.value!!.sortCloudFiles(response) + } + + override fun onError(e: Throwable) { + Log.d(TAG, "An error occurred: $e") + } + + override fun onComplete() { + if (newRemoteFileBrowserItems.isNullOrEmpty()) { + this@RemoteFileBrowserItemsViewModel._viewState.value = NoRemoteFileItemsState + } else { + setCurrentState(newRemoteFileBrowserItems!!) + } + } + + private fun setCurrentState(items: List) { + when (this@RemoteFileBrowserItemsViewModel._viewState.value) { + is LoadedState, LoadingItemsState -> { + this@RemoteFileBrowserItemsViewModel._viewState.value = LoadedState(items) + } + else -> return + } + } + } + + private fun onSelectSortOrder(newSortOrderString: String) { + val newSortOrder = FileSortOrder.getFileSortOrder(newSortOrderString) + if (newSortOrder.name != fileSortOrder.value?.name) { + _fileSortOrder.value = newSortOrder + val currentState = viewState.value + if (currentState is LoadedState) { + val sortedItems = newSortOrder.sortCloudFiles(currentState.items) + _viewState.value = LoadedState(sortedItems) + } + } + } + + private fun changePath(path: String) { + _currentPath.value = path + loadItems() + } + + fun navigateUp() { + val path = _currentPath.value + if (path!! != ROOT_PATH) { + _currentPath.value = File(path).parent!! + loadItems() + } + } + + fun onSelectionDone() { + val selection = selectedPaths.value + if (!selection.isNullOrEmpty()) { + _viewState.value = FinishState(selection) + } + } + + fun onItemClicked(remoteFileBrowserItem: RemoteFileBrowserItem) { + if (remoteFileBrowserItem.mimeType == FOLDER) { + changePath(remoteFileBrowserItem.path!!) + } else { + toggleBrowserItemSelection(remoteFileBrowserItem.path!!) + } + } + + private fun toggleBrowserItemSelection(path: String) { + val paths = selectedPaths.value!!.toMutableSet() + if (paths.contains(path) || shouldPathBeSelectedDueToParent(path)) { + checkAndRemoveAnySelectedParents(path) + } else { + // TODO if it's a folder, remove all the children we added manually + paths.add(path) + _selectedPaths.value = paths + } + } + + private fun checkAndRemoveAnySelectedParents(currentPath: String) { + var file = File(currentPath) + val paths = selectedPaths.value!!.toMutableSet() + paths.remove(currentPath) + while (file.parent != null) { + paths.remove(file.parent!! + File.pathSeparator) + file = File(file.parent!!) + } + _selectedPaths.value = paths + } + + private fun shouldPathBeSelectedDueToParent(currentPath: String): Boolean { + var file = File(currentPath) + val paths = selectedPaths.value!! + if (paths.isNotEmpty() && file.parent != ROOT_PATH) { + while (file.parent != null) { + var parent = file.parent!! + if (File(file.parent!!).parent != null) { + parent += File.pathSeparator + } + if (paths.contains(parent)) { + return true + } + file = File(file.parent!!) + } + } + return false + } + + fun isPathSelected(path: String): Boolean = + selectedPaths.value?.contains(path) == true || shouldPathBeSelectedDueToParent(path) + + companion object { + private val TAG = RemoteFileBrowserItemsViewModel::class.simpleName + private const val ROOT_PATH = "/" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepository.kt new file mode 100644 index 0000000..898ee35 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepository.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.repositories.callrecording + +import com.nextcloud.talk.models.domain.StartCallRecordingModel +import com.nextcloud.talk.models.domain.StopCallRecordingModel +import io.reactivex.Observable + +interface CallRecordingRepository { + + fun startRecording(roomToken: String): Observable + + fun stopRecording(roomToken: String): Observable +} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt new file mode 100644 index 0000000..01c955d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt @@ -0,0 +1,64 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.repositories.callrecording + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.StartCallRecordingModel +import com.nextcloud.talk.models.domain.StopCallRecordingModel +import com.nextcloud.talk.models.json.generic.GenericMeta +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable + +class CallRecordingRepositoryImpl(private val ncApi: NcApi, currentUserProvider: CurrentUserProviderNew) : + CallRecordingRepository { + + val currentUser: User = currentUserProvider.currentUser.blockingGet() + val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!! + + var apiVersion = 1 + + override fun startRecording(roomToken: String): Observable = + ncApi.startRecording( + credentials, + ApiUtils.getUrlForRecording( + apiVersion, + currentUser.baseUrl!!, + roomToken + ), + 1 + ).map { mapToStartCallRecordingModel(it.ocs?.meta!!) } + + override fun stopRecording(roomToken: String): Observable = + ncApi.stopRecording( + credentials, + ApiUtils.getUrlForRecording( + apiVersion, + currentUser.baseUrl!!, + roomToken + ) + ).map { mapToStopCallRecordingModel(it.ocs?.meta!!) } + + private fun mapToStartCallRecordingModel(response: GenericMeta): StartCallRecordingModel { + val success = response.statusCode == HTTP_OK + return StartCallRecordingModel( + success + ) + } + + private fun mapToStopCallRecordingModel(response: GenericMeta): StopCallRecordingModel { + val success = response.statusCode == HTTP_OK + return StopCallRecordingModel( + success + ) + } + + companion object { + private const val HTTP_OK: Int = 200 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt new file mode 100644 index 0000000..cb1e50c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepository.kt @@ -0,0 +1,58 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.repositories.conversations + +import com.nextcloud.talk.conversationinfo.CreateRoomRequest +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.participants.TalkBan +import com.nextcloud.talk.models.json.profile.Profile +import io.reactivex.Observable + +interface ConversationsRepository { + + suspend fun allowGuests(token: String, allow: Boolean): GenericOverall + + data class ResendInvitationsResult(val successful: Boolean) + fun resendInvitations(token: String): Observable + + suspend fun archiveConversation(credentials: String, url: String): GenericOverall + + suspend fun unarchiveConversation(credentials: String, url: String): GenericOverall + + fun setConversationReadOnly(credentials: String, url: String, state: Int): Observable + + suspend fun banActor( + credentials: String, + url: String, + actorType: String, + actorId: String, + internalNote: String + ): TalkBan + + suspend fun listBans(credentials: String, url: String): List + suspend fun unbanActor(credentials: String, url: String): GenericOverall + + suspend fun setPassword(password: String, token: String): GenericOverall + + suspend fun setConversationReadOnly(roomToken: String, state: Int): GenericOverall + + suspend fun clearChatHistory(apiVersion: Int, roomToken: String): GenericOverall + + suspend fun createRoom(credentials: String, url: String, body: CreateRoomRequest): RoomOverall + + suspend fun getProfile(credentials: String, url: String): Profile? + + suspend fun markConversationAsSensitive(credentials: String, baseUrl: String, roomToken: String): GenericOverall + + suspend fun markConversationAsInsensitive(credentials: String, baseUrl: String, roomToken: String): GenericOverall + + suspend fun markConversationAsImportant(credentials: String, baseUrl: String, roomToken: String): GenericOverall + + suspend fun markConversationAsUnImportant(credentials: String, baseUrl: String, roomToken: String): GenericOverall +} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt new file mode 100644 index 0000000..89b56c0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/repositories/conversations/ConversationsRepositoryImpl.kt @@ -0,0 +1,176 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.repositories.conversations + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.conversationinfo.CreateRoomRequest +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.participants.TalkBan +import com.nextcloud.talk.models.json.profile.Profile +import com.nextcloud.talk.repositories.conversations.ConversationsRepository.ResendInvitationsResult +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable + +class ConversationsRepositoryImpl( + private val api: NcApi, + private val coroutineApi: NcApiCoroutines, + private val userProvider: CurrentUserProviderNew +) : ConversationsRepository { + + private val user: User + get() = userProvider.currentUser.blockingGet() + + private val credentials: String + get() = ApiUtils.getCredentials(user.username, user.token)!! + + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) + + override suspend fun allowGuests(token: String, allow: Boolean): GenericOverall { + val url = ApiUtils.getUrlForRoomPublic( + apiVersion, + user.baseUrl!!, + token + ) + + val result: GenericOverall = if (allow) { + coroutineApi.makeRoomPublic( + credentials, + url + ) + } else { + coroutineApi.makeRoomPrivate( + credentials, + url + ) + } + return result + } + + override fun resendInvitations(token: String): Observable { + val apiObservable = api.resendParticipantInvitations( + credentials, + ApiUtils.getUrlForParticipantsResendInvitations( + apiVersion(), + user.baseUrl!!, + token + ) + ) + + return apiObservable.map { + ResendInvitationsResult(true) + } + } + + override suspend fun archiveConversation(credentials: String, url: String): GenericOverall = + coroutineApi.archiveConversation(credentials, url) + + override suspend fun unarchiveConversation(credentials: String, url: String): GenericOverall = + coroutineApi.unarchiveConversation(credentials, url) + + override fun setConversationReadOnly(credentials: String, url: String, state: Int): Observable = + api.setConversationReadOnly(credentials, url, state) + + override suspend fun setConversationReadOnly(roomToken: String, state: Int): GenericOverall { + val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) + val url = ApiUtils.getUrlForConversationReadOnly(apiVersion, user.baseUrl!!, roomToken) + return coroutineApi.setConversationReadOnly(credentials, url, state) + } + + override suspend fun setPassword(password: String, token: String): GenericOverall { + val result = coroutineApi.setPassword( + credentials, + ApiUtils.getUrlForRoomPassword( + apiVersion, + user.baseUrl!!, + token + ), + password + ) + return result + } + + override suspend fun clearChatHistory(apiVersion: Int, roomToken: String): GenericOverall = + coroutineApi.clearChatHistory( + credentials, + ApiUtils.getUrlForChat(apiVersion, user.baseUrl!!, roomToken) + ) + + override suspend fun createRoom(credentials: String, url: String, body: CreateRoomRequest): RoomOverall { + val response = coroutineApi.createRoomWithBody( + credentials, + url, + body + ) + return response + } + + override suspend fun getProfile(credentials: String, url: String): Profile? = + coroutineApi.getProfile(credentials, url).ocs?.data + + override suspend fun markConversationAsSensitive( + credentials: String, + baseUrl: String, + roomToken: String + ): GenericOverall { + val url = ApiUtils.getUrlForSensitiveConversation(baseUrl, roomToken) + return coroutineApi.markConversationAsSensitive(credentials, url) + } + + override suspend fun markConversationAsInsensitive( + credentials: String, + baseUrl: String, + roomToken: String + ): GenericOverall { + val url = ApiUtils.getUrlForSensitiveConversation(baseUrl, roomToken) + return coroutineApi.markConversationAsInsensitive(credentials, url) + } + + override suspend fun markConversationAsImportant( + credentials: String, + baseUrl: String, + roomToken: String + ): GenericOverall { + val url = ApiUtils.getUrlForImportantConversation(baseUrl, roomToken) + return coroutineApi.markConversationAsImportant(credentials, url) + } + + override suspend fun markConversationAsUnImportant( + credentials: String, + baseUrl: String, + roomToken: String + ): GenericOverall { + val url = ApiUtils.getUrlForImportantConversation(baseUrl, roomToken) + return coroutineApi.markConversationAsUnimportant(credentials, url) + } + + override suspend fun banActor( + credentials: String, + url: String, + actorType: String, + actorId: String, + internalNote: String + ): TalkBan = coroutineApi.banActor(credentials, url, actorType, actorId, internalNote) + + override suspend fun listBans(credentials: String, url: String): List { + val talkBanOverall = coroutineApi.listBans(credentials, url) + return talkBanOverall.ocs?.data!! + } + + override suspend fun unbanActor(credentials: String, url: String): GenericOverall = + coroutineApi.unbanActor(credentials, url) + + private fun apiVersion(): Int = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4)) + + companion object { + const val STATUS_CODE_OK = 200 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt new file mode 100644 index 0000000..157df46 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepository.kt @@ -0,0 +1,19 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.repositories.reactions + +import com.nextcloud.talk.models.domain.ReactionAddedModel +import com.nextcloud.talk.models.domain.ReactionDeletedModel +import com.nextcloud.talk.chat.data.model.ChatMessage +import io.reactivex.Observable + +interface ReactionsRepository { + + fun addReaction(roomToken: String, message: ChatMessage, emoji: String): Observable + + fun deleteReaction(roomToken: String, message: ChatMessage, emoji: String): Observable +} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt new file mode 100644 index 0000000..2da7e51 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/repositories/reactions/ReactionsRepositoryImpl.kt @@ -0,0 +1,163 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.repositories.reactions + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ReactionAddedModel +import com.nextcloud.talk.models.domain.ReactionDeletedModel +import com.nextcloud.talk.models.json.generic.GenericMeta +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject + +class ReactionsRepositoryImpl @Inject constructor( + private val ncApi: NcApi, + private val currentUserProvider: CurrentUserProviderNew, + private val dao: ChatMessagesDao +) : ReactionsRepository { + + val currentUser: User = currentUserProvider.currentUser.blockingGet() + val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!! + + override fun addReaction(roomToken: String, message: ChatMessage, emoji: String): Observable { + return ncApi.sendReaction( + credentials, + ApiUtils.getUrlForMessageReaction( + currentUser.baseUrl!!, + roomToken, + message.id + ), + emoji + ).map { + val model = mapToReactionAddedModel(message, emoji, it.ocs?.meta!!) + persistAddedModel(model, roomToken) + return@map model + } + } + + override fun deleteReaction( + roomToken: String, + message: ChatMessage, + emoji: String + ): Observable { + return ncApi.deleteReaction( + credentials, + ApiUtils.getUrlForMessageReaction( + currentUser.baseUrl!!, + roomToken, + message.id + ), + emoji + ).map { + val model = mapToReactionDeletedModel(message, emoji, it.ocs?.meta!!) + persistDeletedModel(model, roomToken) + return@map model + } + } + + private fun mapToReactionAddedModel( + message: ChatMessage, + emoji: String, + reactionResponse: GenericMeta + ): ReactionAddedModel { + val success = reactionResponse.statusCode == HTTP_CREATED + return ReactionAddedModel( + message, + emoji, + success + ) + } + + private fun mapToReactionDeletedModel( + message: ChatMessage, + emoji: String, + reactionResponse: GenericMeta + ): ReactionDeletedModel { + val success = reactionResponse.statusCode == HTTP_OK + return ReactionDeletedModel( + message, + emoji, + success + ) + } + + private fun persistAddedModel(model: ReactionAddedModel, roomToken: String) = + CoroutineScope(Dispatchers.IO).launch { + // 1. Call DAO, Get a singular ChatMessageEntity with model.chatMessage.{PARAM} + val accountId = currentUser.id!! + val id = model.chatMessage.jsonMessageId.toLong() + val internalConversationId = "$accountId@$roomToken" + val emoji = model.emoji + + val message = dao.getChatMessageForConversation( + internalConversationId, + id + ).first() + + // 2. Check state of entity, create params as needed + if (message.reactions == null) { + message.reactions = LinkedHashMap() + } + + if (message.reactionsSelf == null) { + message.reactionsSelf = ArrayList() + } + + var amount = message.reactions!![emoji] + if (amount == null) { + amount = 0 + } + message.reactions!![emoji] = amount + 1 + message.reactionsSelf!!.add(emoji) + + // 3. Call DAO again, to update the singular ChatMessageEntity with params + dao.updateChatMessage(message) + } + + private fun persistDeletedModel(model: ReactionDeletedModel, roomToken: String) = + CoroutineScope(Dispatchers.IO).launch { + // 1. Call DAO, Get a singular ChatMessageEntity with model.chatMessage.{PARAM} + val accountId = currentUser.id!! + val id = model.chatMessage.jsonMessageId.toLong() + val internalConversationId = "$accountId@$roomToken" + val emoji = model.emoji + + val message = dao.getChatMessageForConversation(internalConversationId, id).first() + + // 2. Check state of entity, create params as needed + if (message.reactions == null) { + message.reactions = LinkedHashMap() + } + + if (message.reactionsSelf == null) { + message.reactionsSelf = ArrayList() + } + + var amount = message.reactions!![emoji] + if (amount == null) { + amount = 0 + } + message.reactions!![emoji] = amount - 1 + message.reactionsSelf!!.remove(emoji) + + // 3. Call DAO again, to update the singular ChatMessageEntity with params + dao.updateChatMessage(message) + } + + companion object { + private const val HTTP_OK: Int = 200 + private const val HTTP_CREATED: Int = 201 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepository.kt new file mode 100644 index 0000000..4720493 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepository.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.repositories.unifiedsearch + +import com.nextcloud.talk.models.domain.SearchMessageEntry +import io.reactivex.Observable + +interface UnifiedSearchRepository { + data class UnifiedSearchResults(val cursor: Int, val hasMore: Boolean, val entries: List) + + fun searchMessages( + searchTerm: String, + cursor: Int = 0, + limit: Int = DEFAULT_PAGE_SIZE + ): Observable> + + fun searchInRoom( + roomToken: String, + searchTerm: String, + cursor: Int = 0, + limit: Int = DEFAULT_PAGE_SIZE + ): Observable> + + companion object { + private const val DEFAULT_PAGE_SIZE = 5 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt new file mode 100644 index 0000000..88ce017 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt @@ -0,0 +1,94 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.repositories.unifiedsearch + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.SearchMessageEntry +import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchEntry +import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchResponseData +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable + +class UnifiedSearchRepositoryImpl(private val api: NcApi, private val userProvider: CurrentUserProviderNew) : + UnifiedSearchRepository { + + private val user: User + get() = userProvider.currentUser.blockingGet() + + private val credentials: String + get() = ApiUtils.getCredentials(user.username, user.token)!! + + override fun searchMessages( + searchTerm: String, + cursor: Int, + limit: Int + ): Observable> { + val apiObservable = api.performUnifiedSearch( + credentials, + ApiUtils.getUrlForUnifiedSearch(user.baseUrl!!, PROVIDER_TALK_MESSAGE), + searchTerm, + null, + limit, + cursor + ) + return apiObservable.map { mapToMessageResults(it.ocs?.data!!, searchTerm, limit) } + } + + override fun searchInRoom( + roomToken: String, + searchTerm: String, + cursor: Int, + limit: Int + ): Observable> { + val apiObservable = api.performUnifiedSearch( + credentials, + ApiUtils.getUrlForUnifiedSearch(user.baseUrl!!, PROVIDER_TALK_MESSAGE_CURRENT), + searchTerm, + fromUrlForRoom(roomToken), + limit, + cursor + ) + return apiObservable.map { mapToMessageResults(it.ocs?.data!!, searchTerm, limit) } + } + + private fun fromUrlForRoom(roomToken: String) = "/call/$roomToken" + + companion object { + private const val PROVIDER_TALK_MESSAGE = "talk-message" + private const val PROVIDER_TALK_MESSAGE_CURRENT = "talk-message-current" + + private const val ATTRIBUTE_CONVERSATION = "conversation" + private const val ATTRIBUTE_MESSAGE_ID = "messageId" + + private fun mapToMessageResults( + data: UnifiedSearchResponseData, + searchTerm: String, + limit: Int + ): UnifiedSearchRepository.UnifiedSearchResults { + val entries = data.entries?.map { it -> mapToMessage(it, searchTerm) } + val cursor = data.cursor ?: 0 + val hasMore = entries?.size == limit + return UnifiedSearchRepository.UnifiedSearchResults(cursor, hasMore, entries ?: emptyList()) + } + + private fun mapToMessage(unifiedSearchEntry: UnifiedSearchEntry, searchTerm: String): SearchMessageEntry { + val conversation = unifiedSearchEntry.attributes?.get(ATTRIBUTE_CONVERSATION)!! + val messageId = unifiedSearchEntry.attributes?.get(ATTRIBUTE_MESSAGE_ID) + return SearchMessageEntry( + searchTerm, + unifiedSearchEntry.thumbnailUrl, + unifiedSearchEntry.title!!, + unifiedSearchEntry.subline!!, + conversation, + messageId + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt new file mode 100644 index 0000000..21e8f56 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt @@ -0,0 +1,1441 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2021-2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.settings + +import android.Manifest +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.app.KeyguardManager +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.PorterDuff +import android.media.RingtoneManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.security.KeyChain +import android.text.TextUtils +import android.util.Log +import android.view.View +import android.view.WindowManager +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.toDrawable +import androidx.core.net.toUri +import androidx.core.view.ViewCompat +import androidx.lifecycle.lifecycleScope +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.textfield.TextInputLayout +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.activities.MainActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.setAppTheme +import com.nextcloud.talk.conversationlist.ConversationsListActivity +import com.nextcloud.talk.conversationlist.ConversationsListActivity.Companion.NOTIFICATION_WARNING_DATE_NOT_SET +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivitySettingsBinding +import com.nextcloud.talk.diagnose.DiagnoseActivity +import com.nextcloud.talk.jobs.AccountRemovalWorker +import com.nextcloud.talk.jobs.CapabilitiesWorker +import com.nextcloud.talk.jobs.ContactAddressBookWorker +import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.checkPermission +import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.deleteAll +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.userprofile.UserProfileOverall +import com.nextcloud.talk.profile.ProfileActivity +import com.nextcloud.talk.ui.dialog.SetPhoneNumberDialogFragment +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.ClosedInterfaceImpl +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.DrawableUtils +import com.nextcloud.talk.utils.LoggingUtils.sendMailWithAttachment +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri +import com.nextcloud.talk.utils.NotificationUtils.getMessageRingtoneUri +import com.nextcloud.talk.utils.SecurityUtils +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SCROLL_TO_NOTIFICATION_CATEGORY +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil +import com.nextcloud.talk.utils.power.PowerManagerUtils +import com.nextcloud.talk.utils.preferences.AppPreferencesImpl +import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import retrofit2.HttpException +import java.net.URI +import java.net.URISyntaxException +import java.util.Locale +import javax.inject.Inject + +@Suppress("LargeClass", "TooManyFunctions") +@AutoInjector(NextcloudTalkApplication::class) +class SettingsActivity : + BaseActivity(), + SetPhoneNumberDialogFragment.SetPhoneNumberDialogClickListener { + private lateinit var binding: ActivitySettingsBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var ncApiCoroutines: NcApiCoroutines + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var platformPermissionUtil: PlatformPermissionUtil + + private var currentUser: User? = null + private var credentials: String? = null + private lateinit var proxyTypeFlow: Flow + private lateinit var proxyCredentialFlow: Flow + private lateinit var screenSecurityFlow: Flow + private lateinit var screenLockFlow: Flow + private lateinit var screenLockTimeoutFlow: Flow + private lateinit var themeFlow: Flow + private lateinit var readPrivacyFlow: Flow + private lateinit var typingStatusFlow: Flow + private lateinit var phoneBookIntegrationFlow: Flow + private var profileQueryDisposable: Disposable? = null + private var dbQueryDisposable: Disposable? = null + private var openedByNotificationWarning: Boolean = false + + @SuppressLint("StringFormatInvalid") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivitySettingsBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + initSystemBars() + + binding.avatarImage.let { ViewCompat.setTransitionName(it, "userAvatar.transitionTag") } + + getCurrentUser() + handleIntent(intent) + + setupLicenceSetting() + + binding.settingsScreenLockSummary.text = String.format( + Locale.getDefault(), + resources!!.getString(R.string.nc_settings_screen_lock_desc), + resources!!.getString(R.string.nc_app_product_name) + ) + + setupDiagnose() + setupPrivacyUrl() + setupSourceCodeUrl() + binding.settingsVersionSummary.text = String.format("v" + BuildConfig.VERSION_NAME) + + setupPhoneBookIntegration() + + setupClientCertView() + } + + private fun handleIntent(intent: Intent) { + val extras: Bundle? = intent.extras + openedByNotificationWarning = extras?.getBoolean(KEY_SCROLL_TO_NOTIFICATION_CATEGORY) ?: false + } + + override fun onResume() { + super.onResume() + supportActionBar?.show() + dispose(null) + + loadCapabilitiesAndUpdateSettings() + + binding.settingsVersion.setOnClickListener { + sendLogs() + } + + if (!TextUtils.isEmpty(currentUser!!.clientCertificate)) { + binding.settingsClientCertTitle.setText(R.string.nc_client_cert_change) + } else { + binding.settingsClientCertTitle.setText(R.string.nc_client_cert_setup) + } + + setupCheckables() + setupScreenLockSetting() + setupNotificationSettings() + setupProxyTypeSettings() + setupProxyCredentialSettings() + registerChangeListeners() + + if (currentUser != null) { + binding.domainText.text = currentUser!!.baseUrl!!.toUri().host + setupServerAgeWarning() + if (currentUser!!.displayName != null) { + binding.nameText.text = currentUser!!.displayName + } + DisplayUtils.loadAvatarImage(currentUser, binding.avatarImage, false) + + setupProfileQueryDisposable() + + binding.settingsRemoveAccount.setOnClickListener { + showRemoveAccountWarning() + } + } + setupMessageView() + + binding.settingsName.visibility = View.VISIBLE + binding.settingsName.setOnClickListener { + val intent = Intent(this, ProfileActivity::class.java) + startActivity(intent) + } + + themeTitles() + themeSwitchPreferences() + + if (openedByNotificationWarning) { + scrollToNotificationCategory() + } + } + + @Suppress("MagicNumber") + private fun scrollToNotificationCategory() { + binding.scrollView.post { + val scrollViewLocation = IntArray(2) + val targetLocation = IntArray(2) + binding.scrollView.getLocationOnScreen(scrollViewLocation) + binding.settingsNotificationsCategory.getLocationOnScreen(targetLocation) + val offset = targetLocation[1] - scrollViewLocation[1] + binding.scrollView.scrollBy(0, offset) + } + } + + private fun loadCapabilitiesAndUpdateSettings() { + val capabilitiesWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java).build() + WorkManager.getInstance(context).enqueue(capabilitiesWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(capabilitiesWork.id) + .observe(this) { workInfo -> + if (workInfo?.state == WorkInfo.State.SUCCEEDED) { + getCurrentUser() + setupCheckables() + } + } + } + + private fun setupActionBar() { + setSupportActionBar(binding.settingsToolbar) + binding.settingsToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(android.R.color.transparent, null).toDrawable()) + supportActionBar?.title = context.getString(R.string.nc_settings) + viewThemeUtils.material.themeToolbar(binding.settingsToolbar) + } + + private fun getCurrentUser() { + currentUser = currentUserProvider.currentUser.blockingGet() + credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) + } + + private fun setupPhoneBookIntegration() { + if (CapabilitiesUtil.hasSpreedFeatureCapability( + currentUser?.capabilities?.spreedCapability!!, + SpreedFeatures.PHONEBOOK_SEARCH + ) + ) { + binding.settingsPhoneBookIntegration.visibility = View.VISIBLE + } else { + binding.settingsPhoneBookIntegration.visibility = View.GONE + } + } + + private fun setupNotificationSettings() { + setupNotificationSoundsSettings() + setupNotificationPermissionSettings() + setupServerNotificationAppCheck() + } + + @SuppressLint("StringFormatInvalid") + @Suppress("LongMethod") + private fun setupNotificationPermissionSettings() { + if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) { + binding.settingsGplayOnlyWrapper.visibility = View.VISIBLE + + setTroubleshootingClickListenersIfNecessary() + + if (PowerManagerUtils().isIgnoringBatteryOptimizations()) { + binding.batteryOptimizationIgnored.text = + resources!!.getString(R.string.nc_diagnose_battery_optimization_ignored) + binding.batteryOptimizationIgnored.setTextColor( + resources.getColor(R.color.high_emphasis_text, null) + ) + } else { + binding.batteryOptimizationIgnored.text = + resources!!.getString(R.string.nc_diagnose_battery_optimization_not_ignored) + binding.batteryOptimizationIgnored.setTextColor(resources.getColor(R.color.nc_darkRed, null)) + + if (openedByNotificationWarning) { + DrawableUtils.blinkDrawable(binding.settingsBatteryOptimizationWrapper.background) + } + + binding.settingsBatteryOptimizationWrapper.setOnClickListener { + val dialogText = String.format( + context.resources.getString(R.string.nc_ignore_battery_optimization_dialog_text), + context.resources.getString(R.string.nc_app_name) + ) + + val dialogBuilder = MaterialAlertDialogBuilder(this) + .setTitle(R.string.nc_ignore_battery_optimization_dialog_title) + .setMessage(dialogText) + .setPositiveButton(R.string.nc_ok) { _, _ -> + startActivity( + Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + ) + } + .setNegativeButton(R.string.nc_common_dismiss, null) + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } + + // handle notification permission on API level >= 33 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (platformPermissionUtil.isPostNotificationsPermissionGranted()) { + binding.ncDiagnoseNotificationPermissionSubtitle.text = + resources.getString(R.string.nc_settings_notifications_granted) + binding.ncDiagnoseNotificationPermissionSubtitle.setTextColor( + resources.getColor(R.color.high_emphasis_text, null) + ) + binding.settingsCallSound.isEnabled = true + binding.settingsCallSound.alpha = ENABLED_ALPHA + binding.settingsMessageSound.isEnabled = true + binding.settingsMessageSound.alpha = ENABLED_ALPHA + } else { + binding.ncDiagnoseNotificationPermissionSubtitle.text = + resources.getString(R.string.nc_settings_notifications_declined) + binding.ncDiagnoseNotificationPermissionSubtitle.setTextColor( + resources.getColor(R.color.nc_darkRed, null) + ) + + if (openedByNotificationWarning) { + DrawableUtils.blinkDrawable(binding.settingsNotificationsPermissionWrapper.background) + } + + binding.settingsCallSound.isEnabled = false + binding.settingsCallSound.alpha = DISABLED_ALPHA + binding.settingsMessageSound.isEnabled = false + binding.settingsMessageSound.alpha = DISABLED_ALPHA + + binding.settingsNotificationsPermissionWrapper.setOnClickListener { + requestPermissions( + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + ConversationsListActivity.REQUEST_POST_NOTIFICATIONS_PERMISSION + ) + } + } + } else { + binding.settingsNotificationsPermissionWrapper.visibility = View.GONE + } + } else { + binding.settingsGplayOnlyWrapper.visibility = View.GONE + binding.settingsGplayNotAvailable.visibility = View.VISIBLE + } + } + + private fun setupNotificationSoundsSettings() { + if (NotificationUtils.isCallsNotificationChannelEnabled(this)) { + val callRingtoneUri = getCallRingtoneUri(context, (appPreferences)) + + binding.callsRingtone.setTextColor(resources.getColor(R.color.high_emphasis_text, null)) + binding.callsRingtone.text = getRingtoneName(context, callRingtoneUri) + } else { + binding.callsRingtone.setTextColor( + ResourcesCompat.getColor(context.resources, R.color.nc_darkRed, null) + ) + binding.callsRingtone.text = resources!!.getString(R.string.nc_common_disabled) + + if (openedByNotificationWarning) { + DrawableUtils.blinkDrawable(binding.settingsCallSound.background) + } + } + + if (NotificationUtils.isMessagesNotificationChannelEnabled(this)) { + val messageRingtoneUri = getMessageRingtoneUri(context, (appPreferences)) + binding.messagesRingtone.setTextColor(resources.getColor(R.color.high_emphasis_text, null)) + binding.messagesRingtone.text = getRingtoneName(context, messageRingtoneUri) + } else { + binding.messagesRingtone.setTextColor( + ResourcesCompat.getColor(context.resources, R.color.nc_darkRed, null) + ) + binding.messagesRingtone.text = resources!!.getString(R.string.nc_common_disabled) + + if (openedByNotificationWarning) { + DrawableUtils.blinkDrawable(binding.settingsMessageSound.background) + } + } + + binding.settingsCallSound.setOnClickListener { + val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID) + intent.putExtra( + Settings.EXTRA_CHANNEL_ID, + NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name + ) + + startActivity(intent) + } + binding.settingsMessageSound.setOnClickListener { + val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID) + intent.putExtra( + Settings.EXTRA_CHANNEL_ID, + NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name + ) + startActivity(intent) + } + } + + private fun setTroubleshootingClickListenersIfNecessary() { + fun click() { + val dialogBuilder = MaterialAlertDialogBuilder(this) + .setTitle(R.string.nc_notifications_troubleshooting_dialog_title) + .setMessage(R.string.nc_notifications_troubleshooting_dialog_text) + .setNegativeButton(R.string.nc_diagnose_dialog_open_checklist) { _, _ -> + startActivity( + Intent( + Intent.ACTION_VIEW, + resources.getString(R.string.notification_checklist_url).toUri() + ) + ) + } + .setPositiveButton(R.string.nc_diagnose_dialog_open_dontkillmyapp_website) { _, _ -> + startActivity( + Intent( + Intent.ACTION_VIEW, + resources.getString(R.string.dontkillmyapp_url).toUri() + ) + ) + } + .setNeutralButton(R.string.nc_diagnose_dialog_open_diagnose) { _, _ -> + val intent = Intent(context, DiagnoseActivity::class.java) + startActivity(intent) + } + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE), + dialog.getButton(AlertDialog.BUTTON_NEUTRAL) + ) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (platformPermissionUtil.isPostNotificationsPermissionGranted() && + PowerManagerUtils().isIgnoringBatteryOptimizations() + ) { + binding.settingsNotificationsPermissionWrapper.setOnClickListener { click() } + binding.settingsBatteryOptimizationWrapper.setOnClickListener { click() } + } + } else if (PowerManagerUtils().isIgnoringBatteryOptimizations()) { + binding.settingsBatteryOptimizationWrapper.setOnClickListener { click() } + } + } + + private fun setupServerNotificationAppCheck() { + val serverNotificationAppInstalled = + currentUserProvider.currentUser.blockingGet().capabilities?.notificationsCapability?.features?.isNotEmpty() + ?: false + if (!serverNotificationAppInstalled) { + binding.settingsServerNotificationAppWrapper.visibility = View.VISIBLE + + val description = context.getString(R.string.nc_settings_contact_admin_of) + LINEBREAK + + currentUserProvider.currentUser.blockingGet().baseUrl!! + + binding.settingsServerNotificationAppDescription.text = description + if (openedByNotificationWarning) { + DrawableUtils.blinkDrawable(binding.settingsServerNotificationAppWrapper.background) + } + } else { + binding.settingsServerNotificationAppWrapper.visibility = View.GONE + } + } + + private fun setupSourceCodeUrl() { + if (!TextUtils.isEmpty(resources!!.getString(R.string.nc_source_code_url))) { + binding.settingsSourceCode.setOnClickListener { + startActivity( + Intent( + Intent.ACTION_VIEW, + resources!!.getString(R.string.nc_source_code_url).toUri() + ) + ) + } + } else { + binding.settingsSourceCode.visibility = View.GONE + } + } + + private fun setupDiagnose() { + binding.diagnoseWrapper.setOnClickListener { + val intent = Intent(context, DiagnoseActivity::class.java) + startActivity(intent) + } + } + + private fun setupPrivacyUrl() { + if (!TextUtils.isEmpty(resources!!.getString(R.string.nc_privacy_url))) { + binding.settingsPrivacy.setOnClickListener { + startActivity( + Intent( + Intent.ACTION_VIEW, + resources!!.getString(R.string.nc_privacy_url).toUri() + ) + ) + } + } else { + binding.settingsPrivacy.visibility = View.GONE + } + } + + private fun setupLicenceSetting() { + if (!TextUtils.isEmpty(resources!!.getString(R.string.nc_gpl3_url))) { + binding.settingsLicence.setOnClickListener { + startActivity( + Intent( + Intent.ACTION_VIEW, + resources!!.getString(R.string.nc_gpl3_url).toUri() + ) + ) + } + } else { + binding.settingsLicence.visibility = View.GONE + } + } + + private fun setupClientCertView() { + var host: String? = null + var port = -1 + val uri: URI + try { + uri = URI(currentUser!!.baseUrl!!) + host = uri.host + port = uri.port + Log.d(TAG, "uri is $uri") + } catch (e: URISyntaxException) { + Log.e(TAG, "Failed to create uri") + } + + binding.settingsClientCert.setOnClickListener { + KeyChain.choosePrivateKeyAlias( + this, + { alias: String? -> + var finalAlias: String? = alias + + runOnUiThread { + if (finalAlias != null) { + binding.settingsClientCertTitle.setText(R.string.nc_client_cert_change) + } else { + binding.settingsClientCertTitle.setText(R.string.nc_client_cert_setup) + } + } + + if (finalAlias == null) { + finalAlias = "" + } + Log.d(TAG, "host: $host and port: $port") + currentUser!!.clientCertificate = finalAlias + userManager.updateOrCreateUser(currentUser!!) + }, + arrayOf("RSA", "EC"), + null, + host, + port, + currentUser!!.clientCertificate + ) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun registerChangeListeners() { + val appPreferences = AppPreferencesImpl(context) + proxyTypeFlow = appPreferences.readString(AppPreferencesImpl.PROXY_TYPE) + proxyCredentialFlow = appPreferences.readBoolean(AppPreferencesImpl.PROXY_CRED) + screenSecurityFlow = appPreferences.readBoolean(AppPreferencesImpl.SCREEN_SECURITY) + screenLockFlow = appPreferences.readBoolean(AppPreferencesImpl.SCREEN_LOCK) + screenLockTimeoutFlow = appPreferences.readString(AppPreferencesImpl.SCREEN_LOCK_TIMEOUT) + + val themeKey = context.resources.getString(R.string.nc_settings_theme_key) + themeFlow = appPreferences.readString(themeKey) + + val privacyKey = context.resources.getString(R.string.nc_settings_read_privacy_key) + readPrivacyFlow = appPreferences.readBoolean(privacyKey) + + typingStatusFlow = appPreferences.readBoolean(AppPreferencesImpl.TYPING_STATUS) + phoneBookIntegrationFlow = appPreferences.readBoolean(AppPreferencesImpl.PHONE_BOOK_INTEGRATION) + + var pos = resources.getStringArray(R.array.screen_lock_timeout_entry_values).indexOf( + appPreferences.screenLockTimeout + ) + binding.settingsScreenLockTimeoutLayoutDropdown.setText( + resources.getStringArray(R.array.screen_lock_timeout_descriptions)[pos] + ) + + binding.settingsScreenLockTimeoutLayoutDropdown.setSimpleItems(R.array.screen_lock_timeout_descriptions) + binding.settingsScreenLockTimeoutLayoutDropdown.setOnItemClickListener { _, _, position, _ -> + val entryVal: String = resources.getStringArray(R.array.screen_lock_timeout_entry_values)[position] + appPreferences.screenLockTimeout = entryVal + SecurityUtils.createKey(entryVal) + } + pos = resources.getStringArray(R.array.theme_entry_values).indexOf(appPreferences.theme) + binding.settingsTheme.setText(resources.getStringArray(R.array.theme_descriptions)[pos]) + + binding.settingsTheme.setSimpleItems(R.array.theme_descriptions) + binding.settingsTheme.setOnItemClickListener { _, _, position, _ -> + val entryVal: String = resources.getStringArray(R.array.theme_entry_values)[position] + appPreferences.theme = entryVal + } + + observeProxyType() + observeProxyCredential() + observeScreenSecurity() + observeScreenLock() + observeTheme() + observeReadPrivacy() + observeTypingStatus() + } + + fun sendLogs() { + if (resources!!.getBoolean(R.bool.nc_is_debug)) { + sendMailWithAttachment((context)) + } + } + + private fun showRemoveAccountWarning() { + binding.messageText.context?.let { + val materialAlertDialogBuilder = MaterialAlertDialogBuilder(it) + .setTitle(R.string.nc_settings_remove_account) + .setMessage(R.string.nc_settings_remove_confirmation) + .setPositiveButton(R.string.nc_settings_remove) { _, _ -> + removeCurrentAccount() + } + .setNegativeButton(R.string.nc_cancel) { _, _ -> + // unused atm + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground( + it, + materialAlertDialogBuilder + ) + + val dialog = materialAlertDialogBuilder.show() + + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + } + } + + @SuppressLint("CheckResult", "StringFormatInvalid") + private fun removeCurrentAccount() { + userManager.scheduleUserForDeletionWithId(currentUser!!.id!!).blockingGet() + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo? -> + + when (workInfo?.state) { + WorkInfo.State.SUCCEEDED -> { + val text = String.format( + context.resources.getString(R.string.nc_deleted_user), + currentUser!!.displayName + ) + Toast.makeText( + context, + text, + Toast.LENGTH_LONG + ).show() + restartApp() + } + + WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + Toast.makeText( + context, + context.resources.getString(R.string.nc_common_error_sorry), + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "something went wrong when deleting user with id " + currentUser!!.userId) + restartApp() + } + + else -> {} + } + } + } + + private fun restartApp() { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + + private fun getRingtoneName(context: Context, ringtoneUri: Uri?): String = + if (ringtoneUri == null) { + resources!!.getString(R.string.nc_settings_no_ringtone) + } else if ((NotificationUtils.DEFAULT_CALL_RINGTONE_URI == ringtoneUri.toString()) || + (NotificationUtils.DEFAULT_MESSAGE_RINGTONE_URI == ringtoneUri.toString()) + ) { + resources!!.getString(R.string.nc_settings_default_ringtone) + } else { + val r = RingtoneManager.getRingtone(context, ringtoneUri) + r.getTitle(context) + } + + private fun themeSwitchPreferences() { + binding.run { + listOf( + settingsShowNotificationWarningSwitch, + settingsScreenLockSwitch, + settingsScreenSecuritySwitch, + settingsIncognitoKeyboardSwitch, + settingsPhoneBookIntegrationSwitch, + settingsReadPrivacySwitch, + settingsTypingStatusSwitch, + settingsProxyUseCredentialsSwitch + ).forEach(viewThemeUtils.talk::colorSwitch) + } + } + + private fun themeTitles() { + binding.run { + listOf( + settingsNotificationsTitle, + settingsAboutTitle, + settingsAdvancedTitle, + settingsAppearanceTitle, + settingsPrivacyTitle + ).forEach(viewThemeUtils.platform::colorTextView) + } + } + + private fun setupProxyTypeSettings() { + if (appPreferences.proxyType == null) { + appPreferences.proxyType = resources.getString(R.string.nc_no_proxy) + } + binding.settingsProxyChoice.setText(appPreferences.proxyType) + binding.settingsProxyChoice.setSimpleItems(R.array.proxy_type_descriptions) + binding.settingsProxyChoice.setOnItemClickListener { _, _, position, _ -> + val entryVal = resources.getStringArray(R.array.proxy_type_descriptions)[position] + appPreferences.proxyType = entryVal + } + + binding.settingsProxyHostEdit.setText(appPreferences.proxyHost) + binding.settingsProxyHostEdit.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + appPreferences.proxyHost = binding.settingsProxyHostEdit.text.toString() + } + } + + binding.settingsProxyPortEdit.setText(appPreferences.proxyPort) + binding.settingsProxyPortEdit.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + appPreferences.proxyPort = binding.settingsProxyPortEdit.text.toString() + } + } + binding.settingsProxyUsernameEdit.setText(appPreferences.proxyUsername) + binding.settingsProxyUsernameEdit.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + appPreferences.proxyUsername = binding.settingsProxyUsernameEdit.text.toString() + } + } + binding.settingsProxyPasswordEdit.setText(appPreferences.proxyPassword) + binding.settingsProxyPasswordEdit.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + appPreferences.proxyPassword = binding.settingsProxyPasswordEdit.text.toString() + } + } + + if (((context.resources.getString(R.string.nc_no_proxy)) == appPreferences.proxyType) || + appPreferences.proxyType == null + ) { + hideProxySettings() + } else { + showProxySettings() + } + } + + private fun setupProxyCredentialSettings() { + if (appPreferences.proxyCredentials) { + showProxyCredentials() + } else { + hideProxyCredentials() + } + } + + private fun setupMessageView() { + if (ApplicationWideMessageHolder.getInstance().messageType != null) { + when (ApplicationWideMessageHolder.getInstance().messageType) { + ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED -> { + binding.messageText.let { + viewThemeUtils.platform.colorTextView(it, ColorRole.PRIMARY) + it.text = resources!!.getString(R.string.nc_settings_account_updated) + binding.messageText.visibility = View.VISIBLE + } + } + + ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK -> { + binding.messageText.let { + it.setTextColor(resources!!.getColor(R.color.nc_darkRed, null)) + it.text = resources!!.getString(R.string.nc_settings_wrong_account) + binding.messageText.visibility = View.VISIBLE + viewThemeUtils.platform.colorTextView(it, ColorRole.PRIMARY) + it.text = resources!!.getString(R.string.nc_Server_account_imported) + binding.messageText.visibility = View.VISIBLE + } + } + + ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED -> { + binding.messageText.let { + viewThemeUtils.platform.colorTextView(it, ColorRole.PRIMARY) + it.text = resources!!.getString(R.string.nc_Server_account_imported) + binding.messageText.visibility = View.VISIBLE + } + } + + ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT -> { + binding.messageText.let { + it.setTextColor(resources!!.getColor(R.color.nc_darkRed, null)) + it.text = resources!!.getString(R.string.nc_server_failed_to_import_account) + binding.messageText.visibility = View.VISIBLE + } + } + + else -> binding.messageText.visibility = View.GONE + } + ApplicationWideMessageHolder.getInstance().messageType = null + binding.messageText.animate() + ?.translationY(0f) + ?.alpha(0.0f) + ?.setDuration(DURATION) + ?.setStartDelay(START_DELAY) + ?.setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + binding.messageText.visibility = View.GONE + } + }) + } else { + binding.messageText.visibility = View.GONE + } + } + + private fun setupProfileQueryDisposable() { + profileQueryDisposable = ncApi.getUserProfile( + credentials, + ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl!!) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { userProfileOverall: UserProfileOverall -> + var displayName: String? = null + if (!TextUtils.isEmpty( + userProfileOverall.ocs!!.data!!.displayName + ) + ) { + displayName = userProfileOverall.ocs!!.data!!.displayName + } else if (!TextUtils.isEmpty( + userProfileOverall.ocs!!.data!!.displayNameAlt + ) + ) { + displayName = userProfileOverall.ocs!!.data!!.displayNameAlt + } + if ((!TextUtils.isEmpty(displayName) && !(displayName == currentUser!!.displayName))) { + currentUser!!.displayName = displayName + userManager.updateOrCreateUser(currentUser!!) + binding.nameText.text = currentUser!!.displayName + } + }, + { dispose(profileQueryDisposable) }, + { dispose(profileQueryDisposable) } + ) + } + + private fun setupServerAgeWarning() { + when { + CapabilitiesUtil.isServerEOL(currentUser!!.serverVersion?.major) -> { + binding.serverAgeWarningText.setTextColor(ContextCompat.getColor((context), R.color.nc_darkRed)) + binding.serverAgeWarningText.setText(R.string.nc_settings_server_eol) + binding.serverAgeWarningIcon.setColorFilter( + ContextCompat.getColor((context), R.color.nc_darkRed), + PorterDuff.Mode.SRC_IN + ) + } + + CapabilitiesUtil.isServerAlmostEOL(currentUser!!.serverVersion?.major) -> { + binding.serverAgeWarningText.setTextColor( + ContextCompat.getColor((context), R.color.nc_darkYellow) + ) + binding.serverAgeWarningText.setText(R.string.nc_settings_server_almost_eol) + binding.serverAgeWarningIcon.setColorFilter( + ContextCompat.getColor((context), R.color.nc_darkYellow), + PorterDuff.Mode.SRC_IN + ) + } + + else -> { + binding.serverAgeWarningTextCard.visibility = View.GONE + } + } + } + + private fun setupCheckables() { + binding.settingsShowNotificationWarningSwitch.isChecked = + appPreferences.showRegularNotificationWarning + + if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) { + binding.settingsShowNotificationWarning.setOnClickListener { + val isChecked = binding.settingsShowNotificationWarningSwitch.isChecked + binding.settingsShowNotificationWarningSwitch.isChecked = !isChecked + appPreferences.setShowRegularNotificationWarning(!isChecked) + if (!isChecked) { + appPreferences.setNotificationWarningLastPostponedDate(NOTIFICATION_WARNING_DATE_NOT_SET) + } + } + } else { + binding.settingsShowNotificationWarning.visibility = View.GONE + } + + if (CapabilitiesUtil.isReadStatusAvailable(currentUser!!.capabilities!!.spreedCapability!!)) { + binding.settingsReadPrivacySwitch.isChecked = !CapabilitiesUtil.isReadStatusPrivate(currentUser!!) + } else { + binding.settingsReadPrivacy.visibility = View.GONE + } + + setupTypingStatusSetting() + setupProxyUseSetting() + + binding.settingsScreenLockSwitch.isChecked = appPreferences.isScreenLocked + binding.settingsScreenLock.setOnClickListener { + val isChecked = binding.settingsScreenLockSwitch.isChecked + binding.settingsScreenLockSwitch.isChecked = !isChecked + appPreferences.setScreenLock(!isChecked) + } + + binding.settingsReadPrivacy.setOnClickListener { + val isChecked = binding.settingsReadPrivacySwitch.isChecked + binding.settingsReadPrivacySwitch.isChecked = !isChecked + appPreferences.setReadPrivacy(!isChecked) + } + + binding.settingsIncognitoKeyboardSwitch.isChecked = appPreferences.isKeyboardIncognito + binding.settingsIncognitoKeyboard.setOnClickListener { + val isChecked = binding.settingsIncognitoKeyboardSwitch.isChecked + binding.settingsIncognitoKeyboardSwitch.isChecked = !isChecked + appPreferences.setIncognitoKeyboard(!isChecked) + } + + setupPhoneBookIntegrationSetting() + + binding.settingsScreenSecuritySwitch.isChecked = appPreferences.isScreenSecured + binding.settingsScreenSecurity.setOnClickListener { + val isChecked = binding.settingsScreenSecuritySwitch.isChecked + binding.settingsScreenSecuritySwitch.isChecked = !isChecked + appPreferences.setScreenSecurity(!isChecked) + } + + binding.settingsTypingStatus.setOnClickListener { + val isChecked = binding.settingsTypingStatusSwitch.isChecked + binding.settingsTypingStatusSwitch.isChecked = !isChecked + appPreferences.setTypingStatus(!isChecked) + } + } + + private fun setupPhoneBookIntegrationSetting() { + binding.settingsPhoneBookIntegrationSwitch.isChecked = appPreferences.isPhoneBookIntegrationEnabled + binding.settingsPhoneBookIntegration.setOnClickListener { + val isChecked = binding.settingsPhoneBookIntegrationSwitch.isChecked + binding.settingsPhoneBookIntegrationSwitch.isChecked = !isChecked + appPreferences.setPhoneBookIntegration(!isChecked) + if (!isChecked) { + if (checkPermission(this@SettingsActivity, (context))) { + checkForPhoneNumber() + } + } else { + deleteAll() + } + } + } + + private fun setupProxyUseSetting() { + binding.settingsProxyUseCredentialsSwitch.isChecked = appPreferences.proxyCredentials + binding.settingsProxyUseCredentials.setOnClickListener { + val isChecked = binding.settingsProxyUseCredentialsSwitch.isChecked + binding.settingsProxyUseCredentialsSwitch.isChecked = !isChecked + appPreferences.setProxyNeedsCredentials(!isChecked) + } + } + + private fun setupTypingStatusSetting() { + if (currentUser!!.externalSignalingServer?.externalSignalingServer?.isNotEmpty() == true) { + binding.settingsTypingStatusOnlyWithHpb.visibility = View.GONE + Log.i(TAG, "Typing Status Available: ${CapabilitiesUtil.isTypingStatusAvailable(currentUser!!)}") + + if (CapabilitiesUtil.isTypingStatusAvailable(currentUser!!)) { + binding.settingsTypingStatusSwitch.isChecked = !CapabilitiesUtil.isTypingStatusPrivate(currentUser!!) + } else { + binding.settingsTypingStatus.visibility = View.GONE + } + } else { + Log.i(TAG, "Typing Status not Available") + binding.settingsTypingStatusSwitch.isChecked = false + binding.settingsTypingStatusOnlyWithHpb.visibility = View.VISIBLE + binding.settingsTypingStatus.isEnabled = false + binding.settingsTypingStatusOnlyWithHpb.alpha = DISABLED_ALPHA + binding.settingsTypingStatus.alpha = DISABLED_ALPHA + } + } + + private fun setupScreenLockSetting() { + val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + if (keyguardManager.isKeyguardSecure) { + binding.settingsScreenLock.isEnabled = true + binding.settingsScreenLockTimeout.isEnabled = true + binding.settingsScreenLockSwitch.isChecked = appPreferences.isScreenLocked + binding.settingsScreenLockTimeoutLayoutDropdown.isEnabled = appPreferences.isScreenLocked + if (appPreferences.isScreenLocked) { + binding.settingsScreenLockTimeout.alpha = ENABLED_ALPHA + } else { + binding.settingsScreenLockTimeout.alpha = DISABLED_ALPHA + } + binding.settingsScreenLock.alpha = ENABLED_ALPHA + } else { + binding.settingsScreenLock.isEnabled = false + binding.settingsScreenLockTimeoutLayoutDropdown.isEnabled = false + appPreferences.removeScreenLock() + appPreferences.removeScreenLockTimeout() + binding.settingsScreenLockSwitch.isChecked = false + binding.settingsScreenLock.alpha = DISABLED_ALPHA + binding.settingsScreenLockTimeout.alpha = DISABLED_ALPHA + } + } + + public override fun onDestroy() { + // appPreferences.unregisterProxyTypeListener(proxyTypeChangeListener) + // appPreferences.unregisterProxyCredentialsListener(proxyCredentialsChangeListener) + // appPreferences.unregisterScreenSecurityListener(screenSecurityChangeListener) + // appPreferences.unregisterScreenLockListener(screenLockChangeListener) + // appPreferences.unregisterScreenLockTimeoutListener(screenLockTimeoutChangeListener) + // appPreferences.unregisterThemeChangeListener(themeChangeListener) + // appPreferences.unregisterReadPrivacyChangeListener(readPrivacyChangeListener) + // appPreferences.unregisterTypingStatusChangeListener(typingStatusChangeListener) + // appPreferences.unregisterPhoneBookIntegrationChangeListener(phoneBookIntegrationChangeListener) + + super.onDestroy() + } + + private fun hideProxySettings() { + appPreferences.removeProxyHost() + appPreferences.removeProxyPort() + appPreferences.removeProxyCredentials() + appPreferences.removeProxyUsername() + appPreferences.removeProxyPassword() + binding.settingsProxyHostLayout.visibility = View.GONE + binding.settingsProxyPortLayout.visibility = View.GONE + binding.settingsProxyUseCredentials.visibility = View.GONE + hideProxyCredentials() + } + + private fun showProxySettings() { + binding.settingsProxyHostLayout.visibility = + View.VISIBLE + binding.settingsProxyPortLayout.visibility = + View.VISIBLE + binding.settingsProxyUseCredentials.visibility = + View.VISIBLE + if (binding.settingsProxyUseCredentialsSwitch.isChecked) showProxyCredentials() + } + + private fun showProxyCredentials() { + binding.settingsProxyUsernameLayout.visibility = + View.VISIBLE + binding.settingsProxyPasswordLayout.visibility = + View.VISIBLE + } + + private fun hideProxyCredentials() { + appPreferences.removeProxyUsername() + appPreferences.removeProxyPassword() + binding.settingsProxyUsernameLayout.visibility = View.GONE + binding.settingsProxyPasswordLayout.visibility = View.GONE + } + + private fun dispose(disposable: Disposable?) { + if (disposable != null && !disposable.isDisposed) { + disposable.dispose() + } else if (disposable == null) { + disposeProfileQueryDisposable() + disposeDbQueryDisposable() + } + } + + private fun disposeDbQueryDisposable() { + if (dbQueryDisposable != null && !dbQueryDisposable!!.isDisposed) { + dbQueryDisposable!!.dispose() + dbQueryDisposable = null + } else if (dbQueryDisposable != null) { + dbQueryDisposable = null + } + } + + private fun disposeProfileQueryDisposable() { + if (profileQueryDisposable != null && !profileQueryDisposable!!.isDisposed) { + profileQueryDisposable!!.dispose() + profileQueryDisposable = null + } else if (profileQueryDisposable != null) { + profileQueryDisposable = null + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + when (requestCode) { + ContactAddressBookWorker.REQUEST_PERMISSION -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + WorkManager + .getInstance(this) + .enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java).build()) + checkForPhoneNumber() + } else { + appPreferences.setPhoneBookIntegration(false) + binding.settingsPhoneBookIntegrationSwitch.isChecked = appPreferences.isPhoneBookIntegrationEnabled + Snackbar.make( + binding.root, + context.resources.getString(R.string.no_phone_book_integration_due_to_permissions), + Snackbar.LENGTH_LONG + ).show() + } + } + + ConversationsListActivity.REQUEST_POST_NOTIFICATIONS_PERMISSION -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_DENIED) { + try { + val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, BuildConfig.APPLICATION_ID) + startActivity(intent) + } catch (e: Exception) { + Log.e(TAG, "Failed to open notification settings as fallback", e) + } + } + } + } + } + + private fun observeScreenLock() { + CoroutineScope(Dispatchers.Main).launch { + var state = appPreferences.isScreenLocked + screenLockFlow.collect { newBoolean -> + if (newBoolean != state) { + state = newBoolean + binding.settingsScreenLockTimeout.isEnabled = newBoolean + if (newBoolean) { + binding.settingsScreenLockTimeout.alpha = ENABLED_ALPHA + } else { + binding.settingsScreenLockTimeout.alpha = DISABLED_ALPHA + } + } + } + } + } + + private fun observeScreenSecurity() { + CoroutineScope(Dispatchers.Main).launch { + var state = appPreferences.isScreenSecured + screenSecurityFlow.collect { newBoolean -> + if (newBoolean != state) { + state = newBoolean + if (newBoolean) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + } + } + } + + private fun observeProxyCredential() { + CoroutineScope(Dispatchers.Main).launch { + var state = appPreferences.proxyCredentials + proxyCredentialFlow.collect { newBoolean -> + if (newBoolean != state) { + state = newBoolean + if (newBoolean) { + showProxyCredentials() + } else { + hideProxyCredentials() + } + } + } + } + } + + private fun observeProxyType() { + CoroutineScope(Dispatchers.Main).launch { + var state = appPreferences.proxyType + proxyTypeFlow.collect { newString -> + if (newString != state) { + state = newString + if (((context.resources.getString(R.string.nc_no_proxy)) == newString) || newString.isEmpty()) { + hideProxySettings() + } else { + when (newString) { + "HTTP" -> { + binding.settingsProxyPortEdit.setText(getString(R.string.nc_settings_http_value)) + appPreferences.proxyPort = getString(R.string.nc_settings_http_value) + } + + "DIRECT" -> { + binding.settingsProxyPortEdit.setText(getString(R.string.nc_settings_direct_value)) + appPreferences.proxyPort = getString(R.string.nc_settings_direct_value) + } + + "SOCKS" -> { + binding.settingsProxyPortEdit.setText(getString(R.string.nc_settings_socks_value)) + appPreferences.proxyPort = getString(R.string.nc_settings_socks_value) + } + + else -> { + } + } + showProxySettings() + } + } + } + } + } + + private fun observeTheme() { + CoroutineScope(Dispatchers.Main).launch { + var state = appPreferences.theme + themeFlow.collect { newString -> + if (newString != state) { + state = newString + setAppTheme(newString) + } + } + } + } + + private fun checkForPhoneNumber() { + ncApi.getUserData( + ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token), + ApiUtils.getUrlForUserProfile(currentUser!!.baseUrl!!) + ).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(userProfileOverall: UserProfileOverall) { + if (userProfileOverall.ocs!!.data!!.phone?.isEmpty() == true) { + askForPhoneNumber() + } else { + Log.d(TAG, "phone number already set") + } + } + + override fun onError(e: Throwable) { + // unused atm + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun askForPhoneNumber() { + val dialog = SetPhoneNumberDialogFragment.newInstance() + dialog.show(supportFragmentManager, SetPhoneNumberDialogFragment.TAG) + } + + override fun onSubmitClick(textInputLayout: TextInputLayout, dialog: DialogInterface) { + setPhoneNumber(textInputLayout, dialog) + } + + private fun setPhoneNumber(textInputLayout: TextInputLayout, dialog: DialogInterface) { + val phoneNumber = textInputLayout.editText!!.text.toString() + ncApi.setUserData( + ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token), + ApiUtils.getUrlForUserData(currentUser!!.baseUrl!!, currentUser!!.userId!!), + "phone", + phoneNumber + ).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + when (val statusCode = genericOverall.ocs?.meta?.statusCode) { + HTTP_CODE_OK -> { + dialog.dismiss() + Snackbar.make( + binding.root, + getString( + R.string.nc_settings_phone_book_integration_phone_number_dialog_success + ), + Snackbar.LENGTH_LONG + ).show() + } + + else -> { + textInputLayout.helperText = getString( + R.string.nc_settings_phone_book_integration_phone_number_dialog_invalid + ) + Log.d(TAG, "failed to set phoneNumber. statusCode=$statusCode") + } + } + } + + override fun onError(e: Throwable) { + textInputLayout.helperText = getString( + R.string.nc_settings_phone_book_integration_phone_number_dialog_invalid + ) + Log.e(SetPhoneNumberDialogFragment.TAG, "setPhoneNumber error", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun observeReadPrivacy() { + lifecycleScope.launch { + var state = appPreferences.readPrivacy + readPrivacyFlow.collect { newBoolean -> + if (state != newBoolean) { + state = newBoolean + val booleanValue = if (newBoolean) "0" else "1" + val json = "{\"key\": \"read_status_privacy\", \"value\" : $booleanValue}" + withContext(Dispatchers.IO) { + try { + credentials?.let { credentials -> + ncApiCoroutines.setReadStatusPrivacy( + credentials, + ApiUtils.getUrlForUserSettings(currentUser!!.baseUrl!!), + json.toRequestBody("application/json".toMediaTypeOrNull()) + ) + Log.i(TAG, "reading status set") + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + appPreferences.setReadPrivacy(!newBoolean) + binding.settingsReadPrivacySwitch.isChecked = !newBoolean + } + if (e is HttpException && e.code() == HTTP_ERROR_CODE_BAD_REQUEST) { + Log.e(TAG, "read_status_privacy : Key or value is invalid") + } else { + Log.e(TAG, "Error setting read status", e) + } + } + } + } + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun observeTypingStatus() { + lifecycleScope.launch { + var state = appPreferences.typingStatus + typingStatusFlow.collect { newBoolean -> + if (state != newBoolean) { + state = newBoolean + val booleanValue = if (newBoolean) "0" else "1" + val json = "{\"key\": \"typing_privacy\", \"value\" : $booleanValue}" + withContext(Dispatchers.IO) { + try { + credentials?.let { credentials -> + ncApiCoroutines.setTypingStatusPrivacy( + credentials, + ApiUtils.getUrlForUserSettings(currentUser!!.baseUrl!!), + json.toRequestBody("application/json".toMediaTypeOrNull()) + ) + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + appPreferences.typingStatus = !newBoolean + binding.settingsTypingStatusSwitch.isChecked = !newBoolean + } + if (e is HttpException && e.code() == HTTP_ERROR_CODE_BAD_REQUEST) { + Log.e(TAG, "typing_privacy : Key or value is invalid") + } else { + Log.e(TAG, "Error setting typing status", e) + } + } + } + } + } + } + } + + companion object { + private val TAG = SettingsActivity::class.java.simpleName + private const val DURATION: Long = 2500 + private const val START_DELAY: Long = 5000 + private const val DISABLED_ALPHA: Float = 0.38f + private const val ENABLED_ALPHA: Float = 1.0f + private const val LINEBREAK = "\n" + const val HTTP_CODE_OK: Int = 200 + const val HTTP_ERROR_CODE_BAD_REQUEST: Int = 400 + const val NO_NOTIFICATION_REMINDER_WANTED = 0L + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt b/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt new file mode 100644 index 0000000..598fe4b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/activities/SharedItemsActivity.kt @@ -0,0 +1,236 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.activities + +import android.os.Bundle +import android.util.Log +import android.view.MenuItem +import android.view.View +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import autodagger.AutoInjector +import com.google.android.material.tabs.TabLayout +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivitySharedItemsBinding +import com.nextcloud.talk.shareditems.adapters.SharedItemsAdapter +import com.nextcloud.talk.shareditems.model.SharedItemType +import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class SharedItemsActivity : BaseActivity() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + private lateinit var binding: ActivitySharedItemsBinding + private lateinit var viewModel: SharedItemsViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + val roomToken = intent.getStringExtra(KEY_ROOM_TOKEN)!! + val conversationName = intent.getStringExtra(KEY_CONVERSATION_NAME) + + val user = currentUserProvider.currentUser.blockingGet() + + val isUserConversationOwnerOrModerator = intent.getBooleanExtra(KEY_USER_IS_OWNER_OR_MODERATOR, false) + + binding = ActivitySharedItemsBinding.inflate(layoutInflater) + setSupportActionBar(binding.sharedItemsToolbar) + setContentView(binding.root) + + initSystemBars() + + viewThemeUtils.material.themeToolbar(binding.sharedItemsToolbar) + viewThemeUtils.material.themeTabLayoutOnSurface(binding.sharedItemsTabs) + + supportActionBar?.title = conversationName + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + viewModel = ViewModelProvider(this, viewModelFactory)[SharedItemsViewModel::class.java] + + viewModel.viewState.observe(this) { state -> + handleModelChange(state, user, roomToken, isUserConversationOwnerOrModerator) + } + + binding.imageRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (!recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE) { + viewModel.loadNextItems() + } + } + }) + + viewModel.initialize(user, roomToken) + } + + private fun handleModelChange( + state: SharedItemsViewModel.ViewState?, + user: User, + roomToken: String, + isUserConversationOwnerOrModerator: Boolean + ) { + clearEmptyLoading() + when (state) { + is SharedItemsViewModel.LoadingItemsState, SharedItemsViewModel.InitialState -> { + showLoading() + } + is SharedItemsViewModel.NoSharedItemsState -> { + showEmpty() + } + is SharedItemsViewModel.LoadedState -> { + val sharedMediaItems = state.items + Log.d(TAG, "Items received: $sharedMediaItems") + + val showGrid = state.selectedType == SharedItemType.MEDIA + val layoutManager = if (showGrid) { + GridLayoutManager(this, SPAN_COUNT) + } else { + LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + } + + val adapter = SharedItemsAdapter( + showGrid, + user, + roomToken, + isUserConversationOwnerOrModerator, + viewThemeUtils + ).apply { + items = sharedMediaItems.items + } + binding.imageRecycler.adapter = adapter + binding.imageRecycler.layoutManager = layoutManager + } + is SharedItemsViewModel.TypesLoadedState -> { + initTabs(state.types) + } + else -> {} + } + + viewThemeUtils.material.themeTabLayoutOnSurface(binding.sharedItemsTabs) + } + + private fun clearEmptyLoading() { + binding.sharedItemsTabs.visibility = View.VISIBLE + binding.emptyContainer.emptyListView.visibility = View.GONE + } + + private fun showLoading() { + binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.file_list_loading) + binding.emptyContainer.emptyListView.visibility = View.VISIBLE + } + + private fun showEmpty() { + binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.nc_shared_items_empty) + binding.emptyContainer.emptyListView.visibility = View.VISIBLE + binding.sharedItemsTabs.visibility = View.GONE + } + + private fun initTabs(sharedItemTypes: Set) { + binding.sharedItemsTabs.removeAllTabs() + + if (sharedItemTypes.contains(SharedItemType.MEDIA)) { + val tabMedia: TabLayout.Tab = binding.sharedItemsTabs.newTab() + tabMedia.tag = SharedItemType.MEDIA + tabMedia.setText(R.string.shared_items_media) + binding.sharedItemsTabs.addTab(tabMedia) + } + + if (sharedItemTypes.contains(SharedItemType.FILE)) { + val tabFile: TabLayout.Tab = binding.sharedItemsTabs.newTab() + tabFile.tag = SharedItemType.FILE + tabFile.setText(R.string.shared_items_file) + binding.sharedItemsTabs.addTab(tabFile) + } + + if (sharedItemTypes.contains(SharedItemType.AUDIO)) { + val tabAudio: TabLayout.Tab = binding.sharedItemsTabs.newTab() + tabAudio.tag = SharedItemType.AUDIO + tabAudio.setText(R.string.shared_items_audio) + binding.sharedItemsTabs.addTab(tabAudio) + } + + if (sharedItemTypes.contains(SharedItemType.RECORDING)) { + val tabRecording: TabLayout.Tab = binding.sharedItemsTabs.newTab() + tabRecording.tag = SharedItemType.RECORDING + tabRecording.setText(R.string.shared_items_recording) + binding.sharedItemsTabs.addTab(tabRecording) + } + + if (sharedItemTypes.contains(SharedItemType.VOICE)) { + val tabVoice: TabLayout.Tab = binding.sharedItemsTabs.newTab() + tabVoice.tag = SharedItemType.VOICE + tabVoice.setText(R.string.shared_items_voice) + binding.sharedItemsTabs.addTab(tabVoice) + } + + if (sharedItemTypes.contains(SharedItemType.POLL)) { + val tabVoice: TabLayout.Tab = binding.sharedItemsTabs.newTab() + tabVoice.tag = SharedItemType.POLL + tabVoice.setText(R.string.shared_items_poll) + binding.sharedItemsTabs.addTab(tabVoice) + } + + if (sharedItemTypes.contains(SharedItemType.LOCATION)) { + val tabLocation: TabLayout.Tab = binding.sharedItemsTabs.newTab() + tabLocation.tag = SharedItemType.LOCATION + tabLocation.setText(R.string.nc_shared_items_location) + binding.sharedItemsTabs.addTab(tabLocation) + } + + if (sharedItemTypes.contains(SharedItemType.DECKCARD)) { + val tabDeckCard: TabLayout.Tab = binding.sharedItemsTabs.newTab() + tabDeckCard.tag = SharedItemType.DECKCARD + tabDeckCard.setText(R.string.nc_shared_items_deck_card) + binding.sharedItemsTabs.addTab(tabDeckCard) + } + + if (sharedItemTypes.contains(SharedItemType.OTHER)) { + val tabOther: TabLayout.Tab = binding.sharedItemsTabs.newTab() + tabOther.tag = SharedItemType.OTHER + tabOther.setText(R.string.shared_items_other) + binding.sharedItemsTabs.addTab(tabOther) + } + + binding.sharedItemsTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + viewModel.initialLoadItems(tab.tag as SharedItemType) + } + + override fun onTabUnselected(tab: TabLayout.Tab) = Unit + + override fun onTabReselected(tab: TabLayout.Tab) = Unit + }) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + if (item.itemId == android.R.id.home) { + onBackPressedDispatcher.onBackPressed() + true + } else { + super.onOptionsItemSelected(item) + } + + companion object { + private val TAG = SharedItemsActivity::class.simpleName + const val SPAN_COUNT: Int = 4 + const val KEY_USER_IS_OWNER_OR_MODERATOR = "userIsOwnerOrModerator" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt new file mode 100644 index 0000000..f80bb85 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsAdapter.kt @@ -0,0 +1,89 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.SharedItemGridBinding +import com.nextcloud.talk.databinding.SharedItemListBinding +import com.nextcloud.talk.polls.ui.PollMainDialogFragment +import com.nextcloud.talk.shareditems.activities.SharedItemsActivity +import com.nextcloud.talk.shareditems.model.SharedDeckCardItem +import com.nextcloud.talk.shareditems.model.SharedFileItem +import com.nextcloud.talk.shareditems.model.SharedItem +import com.nextcloud.talk.shareditems.model.SharedLocationItem +import com.nextcloud.talk.shareditems.model.SharedOtherItem +import com.nextcloud.talk.shareditems.model.SharedPollItem +import com.nextcloud.talk.ui.theme.ViewThemeUtils + +class SharedItemsAdapter( + private val showGrid: Boolean, + private val user: User, + private val roomToken: String, + private val isUserConversationOwnerOrModerator: Boolean, + private val viewThemeUtils: ViewThemeUtils +) : RecyclerView.Adapter() { + + var items: List = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SharedItemsViewHolder = + if (showGrid) { + SharedItemsGridViewHolder( + SharedItemGridBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + user, + viewThemeUtils + ) + } else { + SharedItemsListViewHolder( + SharedItemListBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + user, + viewThemeUtils + ) + } + + override fun onBindViewHolder(holder: SharedItemsViewHolder, position: Int) { + when (val item = items[position]) { + is SharedPollItem -> holder.onBind(item, ::showPoll) + is SharedFileItem -> holder.onBind(item) + is SharedLocationItem -> holder.onBind(item) + is SharedOtherItem -> holder.onBind(item) + is SharedDeckCardItem -> holder.onBind(item) + } + } + + override fun getItemCount(): Int = items.size + + private fun showPoll(item: SharedItem, context: Context) { + val pollVoteDialog = PollMainDialogFragment.newInstance( + user, + roomToken, + isUserConversationOwnerOrModerator, + item.id, + item.name + ) + pollVoteDialog.show( + (context as SharedItemsActivity).supportFragmentManager, + TAG + ) + } + + companion object { + private val TAG = SharedItemsAdapter::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt new file mode 100644 index 0000000..79b8f31 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsGridViewHolder.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.adapters + +import android.view.View +import android.widget.ImageView +import android.widget.ProgressBar +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.SharedItemGridBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils + +class SharedItemsGridViewHolder( + override val binding: SharedItemGridBinding, + user: User, + viewThemeUtils: ViewThemeUtils +) : SharedItemsViewHolder(binding, user, viewThemeUtils) { + + override val image: ImageView + get() = binding.image + override val clickTarget: View + get() = binding.image + override val progressBar: ProgressBar + get() = binding.progressBar +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt new file mode 100644 index 0000000..232b081 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsListViewHolder.kt @@ -0,0 +1,130 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.adapters + +import android.content.Context +import android.content.Intent +import android.text.format.Formatter +import android.view.View +import android.widget.ImageView +import android.widget.ProgressBar +import androidx.core.content.ContextCompat +import coil.load +import com.nextcloud.talk.R +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.SharedItemListBinding +import com.nextcloud.talk.shareditems.model.SharedDeckCardItem +import com.nextcloud.talk.shareditems.model.SharedFileItem +import com.nextcloud.talk.shareditems.model.SharedItem +import com.nextcloud.talk.shareditems.model.SharedLocationItem +import com.nextcloud.talk.shareditems.model.SharedOtherItem +import com.nextcloud.talk.shareditems.model.SharedPollItem +import com.nextcloud.talk.ui.theme.ViewThemeUtils + +class SharedItemsListViewHolder( + override val binding: SharedItemListBinding, + user: User, + viewThemeUtils: ViewThemeUtils +) : SharedItemsViewHolder(binding, user, viewThemeUtils) { + + override val image: ImageView + get() = binding.fileImage + override val clickTarget: View + get() = binding.fileItem + override val progressBar: ProgressBar + get() = binding.progressBar + + override fun onBind(item: SharedFileItem) { + super.onBind(item) + + binding.fileName.text = item.name + binding.fileSize.text = item.fileSize.let { + Formatter.formatShortFileSize( + binding.fileSize.context, + it + ) + } + binding.fileDate.text = item.dateTime + binding.actor.text = item.actorName + } + + override fun onBind(item: SharedPollItem, showPoll: (item: SharedItem, context: Context) -> Unit) { + super.onBind(item, showPoll) + + binding.fileName.text = item.name + binding.fileSize.visibility = View.GONE + binding.separator1.visibility = View.GONE + binding.fileDate.text = item.dateTime + binding.actor.text = item.actorName + image.load(R.drawable.ic_baseline_bar_chart_24) + image.setColorFilter( + ContextCompat.getColor(image.context, R.color.high_emphasis_menu_icon), + android.graphics.PorterDuff.Mode.SRC_IN + ) + clickTarget.setOnClickListener { + showPoll(item, it.context) + } + } + + override fun onBind(item: SharedLocationItem) { + super.onBind(item) + + binding.fileName.text = item.name + binding.fileSize.visibility = View.GONE + binding.separator1.visibility = View.GONE + binding.fileDate.text = item.dateTime + binding.actor.text = item.actorName + image.load(R.drawable.ic_baseline_location_on_24) + image.setColorFilter( + ContextCompat.getColor(image.context, R.color.high_emphasis_menu_icon), + android.graphics.PorterDuff.Mode.SRC_IN + ) + + clickTarget.setOnClickListener { + val browserIntent = Intent(Intent.ACTION_VIEW, item.geoUri) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + it.context.startActivity(browserIntent) + } + } + + override fun onBind(item: SharedOtherItem) { + super.onBind(item) + + binding.fileName.text = item.name + binding.fileSize.visibility = View.GONE + binding.separator1.visibility = View.GONE + binding.fileDate.text = item.dateTime + binding.actor.text = item.actorName + image.load(R.drawable.ic_mimetype_file) + image.setColorFilter( + ContextCompat.getColor(image.context, R.color.high_emphasis_menu_icon), + android.graphics.PorterDuff.Mode.SRC_IN + ) + } + + override fun onBind(item: SharedDeckCardItem) { + super.onBind(item) + + binding.fileName.text = item.name + binding.fileSize.visibility = View.GONE + binding.separator1.visibility = View.GONE + binding.fileDate.text = item.dateTime + binding.actor.text = item.actorName + image.load(R.drawable.ic_baseline_deck_24) + image.setColorFilter( + ContextCompat.getColor(image.context, R.color.high_emphasis_menu_icon), + android.graphics.PorterDuff.Mode.SRC_IN + ) + + clickTarget.setOnClickListener { + val browserIntent = Intent(Intent.ACTION_VIEW, item.link) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + it.context.startActivity(browserIntent) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt new file mode 100644 index 0000000..75628ca --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt @@ -0,0 +1,92 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.adapters + +import android.content.Context +import android.view.View +import android.widget.ImageView +import android.widget.ProgressBar +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.extensions.loadImage +import com.nextcloud.talk.shareditems.model.SharedDeckCardItem +import com.nextcloud.talk.shareditems.model.SharedFileItem +import com.nextcloud.talk.shareditems.model.SharedItem +import com.nextcloud.talk.shareditems.model.SharedLocationItem +import com.nextcloud.talk.shareditems.model.SharedOtherItem +import com.nextcloud.talk.shareditems.model.SharedPollItem +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.FileViewerUtils + +abstract class SharedItemsViewHolder( + open val binding: ViewBinding, + internal val user: User, + private val viewThemeUtils: ViewThemeUtils +) : RecyclerView.ViewHolder(binding.root) { + + companion object { + private val TAG = SharedItemsViewHolder::class.simpleName + } + + abstract val image: ImageView + abstract val clickTarget: View + abstract val progressBar: ProgressBar + + open fun onBind(item: SharedFileItem) { + val placeholder = viewThemeUtils.talk.getPlaceholderImage(image.context, item.mimeType) + if (item.previewAvailable) { + image.loadImage( + item.previewLink, + user, + placeholder + ) + } else { + image.setImageDrawable(placeholder) + } + + /* + The FileViewerUtils forces us to do things at this points which should be done separated in the activity and + the view model. + + This should be done after a refactoring of FileViewerUtils. + */ + val fileViewerUtils = FileViewerUtils(image.context, user) + + clickTarget.setOnClickListener { + fileViewerUtils.openFile( + FileViewerUtils.FileInfo(item.id, item.name, item.fileSize), + item.path, + item.link, + item.mimeType, + FileViewerUtils.ProgressUi( + progressBar, + null, + image + ), + true + ) + } + + fileViewerUtils.resumeToUpdateViewsByProgress( + item.name, + item.id, + item.mimeType, + true, + FileViewerUtils.ProgressUi(progressBar, null, image) + ) + } + + open fun onBind(item: SharedPollItem, showPoll: (item: SharedItem, context: Context) -> Unit) {} + + open fun onBind(item: SharedLocationItem) {} + + open fun onBind(item: SharedOtherItem) {} + + open fun onBind(item: SharedDeckCardItem) {} +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedDeckCardItem.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedDeckCardItem.kt new file mode 100644 index 0000000..b27cd52 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedDeckCardItem.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.model + +import android.net.Uri + +data class SharedDeckCardItem( + override val id: String, + override val name: String, + override val actorId: String, + override val actorName: String, + override val dateTime: String, + val link: Uri +) : SharedItem diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedFileItem.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedFileItem.kt new file mode 100644 index 0000000..17b09c7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedFileItem.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.model + +data class SharedFileItem( + override val id: String, + override val name: String, + override val actorId: String, + override val actorName: String, + override val dateTime: String, + val fileSize: Long, + val path: String, + val link: String, + val mimeType: String, + val previewAvailable: Boolean = false, + val previewLink: String +) : SharedItem diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItem.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItem.kt new file mode 100644 index 0000000..2f22c9b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItem.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.model + +interface SharedItem { + val id: String + val name: String + val actorId: String + val actorName: String + val dateTime: String +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItemType.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItemType.kt new file mode 100644 index 0000000..2d90036 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItemType.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.model + +import java.util.Locale + +enum class SharedItemType { + + AUDIO, + FILE, + MEDIA, + RECORDING, + VOICE, + LOCATION, + DECKCARD, + OTHER, + POLL; + + companion object { + fun typeFor(name: String) = valueOf(name.uppercase(Locale.ROOT)) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItems.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItems.kt new file mode 100644 index 0000000..5907359 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedItems.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.model + +class SharedItems( + val items: List, + val type: SharedItemType, + var lastSeenId: Int?, + var moreItemsExisting: Boolean +) diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedLocationItem.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedLocationItem.kt new file mode 100644 index 0000000..26e3f0d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedLocationItem.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.model + +import android.net.Uri + +data class SharedLocationItem( + override val id: String, + override val name: String, + override val actorId: String, + override val actorName: String, + override val dateTime: String, + val geoUri: Uri +) : SharedItem diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedOtherItem.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedOtherItem.kt new file mode 100644 index 0000000..ec46272 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedOtherItem.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.model + +data class SharedOtherItem( + override val id: String, + override val name: String, + override val actorId: String, + override val actorName: String, + override val dateTime: String +) : SharedItem diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedPollItem.kt b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedPollItem.kt new file mode 100644 index 0000000..5ac47ed --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/model/SharedPollItem.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.model + +data class SharedPollItem( + override val id: String, + override val name: String, + override val actorId: String, + override val actorName: String, + override val dateTime: String +) : SharedItem diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt new file mode 100644 index 0000000..bd4f644 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.repositories + +import com.nextcloud.talk.shareditems.model.SharedItemType +import com.nextcloud.talk.shareditems.model.SharedItems +import io.reactivex.Observable + +interface SharedItemsRepository { + + fun media(parameters: Parameters, type: SharedItemType): Observable? + + fun media(parameters: Parameters, type: SharedItemType, lastKnownMessageId: Int?): Observable? + + fun availableTypes(parameters: Parameters): Observable> + + data class Parameters(val userName: String, val userToken: String, val baseUrl: String, val roomToken: String) +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt new file mode 100644 index 0000000..333520c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepositoryImpl.kt @@ -0,0 +1,206 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.repositories + +import android.util.Log +import androidx.core.net.toUri +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.models.json.chat.ChatShareOverall +import com.nextcloud.talk.shareditems.model.SharedDeckCardItem +import com.nextcloud.talk.shareditems.model.SharedFileItem +import com.nextcloud.talk.shareditems.model.SharedItem +import com.nextcloud.talk.shareditems.model.SharedItemType +import com.nextcloud.talk.shareditems.model.SharedItems +import com.nextcloud.talk.shareditems.model.SharedLocationItem +import com.nextcloud.talk.shareditems.model.SharedOtherItem +import com.nextcloud.talk.shareditems.model.SharedPollItem +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DateConstants +import com.nextcloud.talk.utils.DateUtils +import io.reactivex.Observable +import retrofit2.Response +import java.util.Locale +import javax.inject.Inject + +class SharedItemsRepositoryImpl @Inject constructor(private val ncApi: NcApi, private val dateUtils: DateUtils) : + SharedItemsRepository { + + override fun media(parameters: SharedItemsRepository.Parameters, type: SharedItemType): Observable? = + media(parameters, type, null) + + override fun media( + parameters: SharedItemsRepository.Parameters, + type: SharedItemType, + lastKnownMessageId: Int? + ): Observable? { + val credentials = ApiUtils.getCredentials(parameters.userName, parameters.userToken) + + return ncApi.getSharedItems( + credentials, + ApiUtils.getUrlForChatSharedItems(1, parameters.baseUrl, parameters.roomToken), + type.toString().lowercase(Locale.ROOT), + lastKnownMessageId, + BATCH_SIZE + ).map { map(it, parameters, type) } + } + + private fun map( + response: Response, + parameters: SharedItemsRepository.Parameters, + type: SharedItemType + ): SharedItems { + var chatLastGiven: Int? = null + val items = mutableMapOf() + + if (response.headers()["x-chat-last-given"] != null) { + chatLastGiven = response.headers()["x-chat-last-given"]!!.toInt() + } + + val mediaItems = response.body()!!.ocs!!.data + if (mediaItems != null) { + for (it in mediaItems) { + val actorParameters = it.value.messageParameters!!["actor"]!! + val dateTime = dateUtils.getLocalDateTimeStringFromTimestamp( + it.value.timestamp * DateConstants.SECOND_DIVIDER + ) + + if (it.value.messageParameters?.containsKey("file") == true) { + val fileParameters = it.value.messageParameters!!["file"]!! + + val previewAvailable = + "yes".equals(fileParameters["preview-available"]!!, ignoreCase = true) + + items[it.value.id.toString()] = SharedFileItem( + fileParameters["id"]!!, + fileParameters["name"]!!, + actorParameters["id"]!!, + actorParameters["name"]!!, + dateTime, + fileParameters["size"]!!.toLong(), + fileParameters["path"]!!, + fileParameters["link"]!!, + fileParameters["mimetype"]!!, + previewAvailable, + previewLink(fileParameters["id"], parameters.baseUrl!!) + ) + } else if (it.value.messageParameters?.containsKey("object") == true) { + val objectParameters = it.value.messageParameters!!["object"]!! + items[it.value.id.toString()] = itemFromObject(objectParameters, actorParameters, dateTime) + } else { + Log.w(TAG, "Item contains neither 'file' or 'object'.") + } + } + } + + val sortedMutableItems = items.toSortedMap().values.toList().reversed().toMutableList() + val moreItemsExisting = items.count() == BATCH_SIZE + + return SharedItems( + sortedMutableItems, + type, + chatLastGiven, + moreItemsExisting + ) + } + + private fun itemFromObject( + objectParameters: HashMap, + actorParameters: HashMap, + dateTime: String + ): SharedItem { + val returnValue: SharedItem + when (objectParameters["type"]) { + "talk-poll" -> { + returnValue = SharedPollItem( + objectParameters["id"]!!, + objectParameters["name"]!!, + actorParameters["id"]!!, + actorParameters["name"]!!, + dateTime + ) + } + + "geo-location" -> { + returnValue = SharedLocationItem( + objectParameters["id"]!!, + objectParameters["name"]!!, + actorParameters["id"]!!, + actorParameters["name"]!!, + dateTime, + objectParameters["id"]!!.replace("geo:", "geo:0,0?z=11&q=").toUri() + ) + } + + "deck-card" -> { + returnValue = SharedDeckCardItem( + objectParameters["id"]!!, + objectParameters["name"]!!, + actorParameters["id"]!!, + actorParameters["name"]!!, + dateTime, + objectParameters["link"]!!.toUri() + ) + } + + else -> { + returnValue = SharedOtherItem( + objectParameters["id"]!!, + objectParameters["name"]!!, + actorParameters["id"]!!, + actorParameters["name"]!!, + dateTime + ) + } + } + return returnValue + } + + override fun availableTypes(parameters: SharedItemsRepository.Parameters): Observable> { + val credentials = ApiUtils.getCredentials(parameters.userName, parameters.userToken) + + return ncApi.getSharedItemsOverview( + credentials, + ApiUtils.getUrlForChatSharedItemsOverview(1, parameters.baseUrl, parameters.roomToken), + 1 + ).map { + val types = mutableSetOf() + + if (it.code() == HTTP_OK) { + val typeMap = it.body()!!.ocs!!.data!! + for (t in typeMap) { + if (t.value.isNotEmpty()) { + try { + types += SharedItemType.typeFor(t.key) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Server responds an unknown shared item type: ${t.key}") + } + } + } + } else { + Log.e(TAG, "Failed to getSharedItemsOverview") + } + + types.toSet() + } + } + + private fun previewLink(fileId: String?, baseUrl: String): String = + ApiUtils.getUrlForFilePreviewWithFileId( + baseUrl, + fileId!!, + sharedApplication!!.resources.getDimensionPixelSize(R.dimen.maximum_file_preview_size) + ) + + companion object { + const val BATCH_SIZE: Int = 28 + private const val HTTP_OK: Int = 200 + private val TAG = SharedItemsRepositoryImpl::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt b/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt new file mode 100644 index 0000000..3de476c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/shareditems/viewmodels/SharedItemsViewModel.kt @@ -0,0 +1,163 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.shareditems.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.shareditems.model.SharedItemType +import com.nextcloud.talk.shareditems.model.SharedItems +import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class SharedItemsViewModel @Inject constructor(private val repository: SharedItemsRepository) : ViewModel() { + + private lateinit var repositoryParameters: SharedItemsRepository.Parameters + + sealed interface ViewState + object InitialState : ViewState + object NoSharedItemsState : ViewState + open class TypesLoadedState(val types: Set, val selectedType: SharedItemType) : ViewState + class LoadingItemsState(types: Set, selectedType: SharedItemType) : + TypesLoadedState(types, selectedType) + + class LoadedState(types: Set, selectedType: SharedItemType, val items: SharedItems) : + TypesLoadedState(types, selectedType) + + private val _viewState: MutableLiveData = MutableLiveData(InitialState) + val viewState: LiveData + get() = _viewState + + fun initialize(user: User, roomToken: String) { + repositoryParameters = SharedItemsRepository.Parameters( + user.userId!!, + user.token!!, + user.baseUrl!!, + roomToken + ) + loadAvailableTypes() + } + + private fun loadAvailableTypes() { + repository.availableTypes(repositoryParameters).subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer> { + + var types: Set? = null + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(types: Set) { + this.types = types + } + + override fun onError(e: Throwable) { + Log.e(TAG, "An error occurred when loading available types", e) + } + + override fun onComplete() { + val newTypes = this.types + if (newTypes.isNullOrEmpty()) { + this@SharedItemsViewModel._viewState.value = NoSharedItemsState + } else { + val selectedType = chooseInitialType(newTypes) + this@SharedItemsViewModel._viewState.value = + TypesLoadedState(newTypes, selectedType) + initialLoadItems(selectedType) + } + } + }) + } + + private fun chooseInitialType(newTypes: Set): SharedItemType = + when { + newTypes.contains(SharedItemType.MEDIA) -> SharedItemType.MEDIA + else -> newTypes.toList().first() + } + + fun initialLoadItems(type: SharedItemType) { + val state = _viewState.value + if (state is TypesLoadedState) { + _viewState.value = LoadingItemsState(state.types, type) + repository.media(repositoryParameters, type)?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(SharedMediaItemsObserver()) + } + } + + fun loadNextItems() { + when (val currentState = _viewState.value) { + is LoadedState -> { + val currentSharedItems = currentState.items + if (currentSharedItems.moreItemsExisting) { + repository.media(repositoryParameters, currentState.selectedType, currentSharedItems.lastSeenId) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(SharedMediaItemsObserver()) + } + } + else -> return + } + } + + inner class SharedMediaItemsObserver : Observer { + + var newSharedItems: SharedItems? = null + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(response: SharedItems) { + newSharedItems = response + } + + override fun onError(e: Throwable) { + Log.d(TAG, "An error occurred: $e") + } + + override fun onComplete() { + val items = newSharedItems!! + val state = this@SharedItemsViewModel._viewState.value + if (state is LoadedState) { + val oldItems = state.items.items + val newItems = + SharedItems( + oldItems + newSharedItems!!.items, + state.items.type, + newSharedItems!!.lastSeenId, + newSharedItems!!.moreItemsExisting + ) + setCurrentState(newItems) + } else { + setCurrentState(items) + } + } + + private fun setCurrentState(items: SharedItems) { + when (val state = this@SharedItemsViewModel._viewState.value) { + is TypesLoadedState -> { + this@SharedItemsViewModel._viewState.value = LoadedState( + state.types, + state.selectedType, + items + ) + } + else -> return + } + } + } + + companion object { + private val TAG = SharedItemsViewModel::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/CallParticipantMessageNotifier.java b/app/src/main/java/com/nextcloud/talk/signaling/CallParticipantMessageNotifier.java new file mode 100644 index 0000000..8fd8968 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/CallParticipantMessageNotifier.java @@ -0,0 +1,94 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Helper class to register and notify CallParticipantMessageListeners. + *

+ * This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against + * a SignalingMessageReceiver rather than against a CallParticipantMessageNotifier. + */ +class CallParticipantMessageNotifier { + + private final List callParticipantMessageListenersFrom = new ArrayList<>(); + + /** + * Helper class to associate a CallParticipantMessageListener with a session ID. + */ + private static class CallParticipantMessageListenerFrom { + public final SignalingMessageReceiver.CallParticipantMessageListener listener; + public final String sessionId; + + private CallParticipantMessageListenerFrom(SignalingMessageReceiver.CallParticipantMessageListener listener, + String sessionId) { + this.listener = listener; + this.sessionId = sessionId; + } + } + + public synchronized void addListener(SignalingMessageReceiver.CallParticipantMessageListener listener, String sessionId) { + if (listener == null) { + throw new IllegalArgumentException("CallParticipantMessageListener can not be null"); + } + + if (sessionId == null) { + throw new IllegalArgumentException("sessionId can not be null"); + } + + removeListener(listener); + + callParticipantMessageListenersFrom.add(new CallParticipantMessageListenerFrom(listener, sessionId)); + } + + public synchronized void removeListener(SignalingMessageReceiver.CallParticipantMessageListener listener) { + Iterator it = callParticipantMessageListenersFrom.iterator(); + while (it.hasNext()) { + CallParticipantMessageListenerFrom listenerFrom = it.next(); + + if (listenerFrom.listener == listener) { + it.remove(); + + return; + } + } + } + + private List getListenersFor(String sessionId) { + List callParticipantMessageListeners = + new ArrayList<>(callParticipantMessageListenersFrom.size()); + + for (CallParticipantMessageListenerFrom listenerFrom : callParticipantMessageListenersFrom) { + if (listenerFrom.sessionId.equals(sessionId)) { + callParticipantMessageListeners.add(listenerFrom.listener); + } + } + + return callParticipantMessageListeners; + } + + public synchronized void notifyRaiseHand(String sessionId, boolean state, long timestamp) { + for (SignalingMessageReceiver.CallParticipantMessageListener listener : getListenersFor(sessionId)) { + listener.onRaiseHand(state, timestamp); + } + } + + public void notifyReaction(String sessionId, String reaction) { + for (SignalingMessageReceiver.CallParticipantMessageListener listener : getListenersFor(sessionId)) { + listener.onReaction(reaction); + } + } + + public synchronized void notifyUnshareScreen(String sessionId) { + for (SignalingMessageReceiver.CallParticipantMessageListener listener : getListenersFor(sessionId)) { + listener.onUnshareScreen(); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt b/app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt new file mode 100644 index 0000000..9bd2408 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/ConversationMessageNotifier.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling + +import com.nextcloud.talk.signaling.SignalingMessageReceiver.ConversationMessageListener + +internal class ConversationMessageNotifier { + private val conversationMessageListeners: MutableSet = LinkedHashSet() + + @Synchronized + fun addListener(listener: ConversationMessageListener?) { + requireNotNull(listener) { "conversationMessageListener can not be null" } + conversationMessageListeners.add(listener) + } + + @Synchronized + fun removeListener(listener: ConversationMessageListener) { + conversationMessageListeners.remove(listener) + } + + @Synchronized + fun notifyStartTyping(userId: String?, sessionId: String?) { + for (listener in ArrayList(conversationMessageListeners)) { + listener.onStartTyping(userId, sessionId) + } + } + + fun notifyStopTyping(userId: String?, sessionId: String?) { + for (listener in ArrayList(conversationMessageListeners)) { + listener.onStopTyping(userId, sessionId) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/LocalParticipantMessageNotifier.java b/app/src/main/java/com/nextcloud/talk/signaling/LocalParticipantMessageNotifier.java new file mode 100644 index 0000000..154a1aa --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/LocalParticipantMessageNotifier.java @@ -0,0 +1,40 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Helper class to register and notify LocalParticipantMessageListeners. + *

+ * This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against + * a SignalingMessageReceiver rather than against a LocalParticipantMessageNotifier. + */ +class LocalParticipantMessageNotifier { + + private final Set localParticipantMessageListeners = new LinkedHashSet<>(); + + public synchronized void addListener(SignalingMessageReceiver.LocalParticipantMessageListener listener) { + if (listener == null) { + throw new IllegalArgumentException("localParticipantMessageListener can not be null"); + } + + localParticipantMessageListeners.add(listener); + } + + public synchronized void removeListener(SignalingMessageReceiver.LocalParticipantMessageListener listener) { + localParticipantMessageListeners.remove(listener); + } + + public synchronized void notifySwitchTo(String token) { + for (SignalingMessageReceiver.LocalParticipantMessageListener listener : new ArrayList<>(localParticipantMessageListeners)) { + listener.onSwitchTo(token); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/OfferMessageNotifier.java b/app/src/main/java/com/nextcloud/talk/signaling/OfferMessageNotifier.java new file mode 100644 index 0000000..f8947b5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/OfferMessageNotifier.java @@ -0,0 +1,40 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Helper class to register and notify OfferMessageListeners. + *

+ * This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against + * a SignalingMessageReceiver rather than against an OfferMessageNotifier. + */ +class OfferMessageNotifier { + + private final Set offerMessageListeners = new LinkedHashSet<>(); + + public synchronized void addListener(SignalingMessageReceiver.OfferMessageListener listener) { + if (listener == null) { + throw new IllegalArgumentException("OfferMessageListener can not be null"); + } + + offerMessageListeners.add(listener); + } + + public synchronized void removeListener(SignalingMessageReceiver.OfferMessageListener listener) { + offerMessageListeners.remove(listener); + } + + public synchronized void notifyOffer(String sessionId, String roomType, String sdp, String nick) { + for (SignalingMessageReceiver.OfferMessageListener listener : new ArrayList<>(offerMessageListeners)) { + listener.onOffer(sessionId, roomType, sdp, nick); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/ParticipantListMessageNotifier.java b/app/src/main/java/com/nextcloud/talk/signaling/ParticipantListMessageNotifier.java new file mode 100644 index 0000000..fe8622f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/ParticipantListMessageNotifier.java @@ -0,0 +1,55 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import com.nextcloud.talk.models.json.participants.Participant; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Helper class to register and notify ParticipantListMessageListeners. + *

+ * This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against + * a SignalingMessageReceiver rather than against a ParticipantListMessageNotifier. + */ +class ParticipantListMessageNotifier { + + private final Set participantListMessageListeners = new LinkedHashSet<>(); + + public synchronized void addListener(SignalingMessageReceiver.ParticipantListMessageListener listener) { + if (listener == null) { + throw new IllegalArgumentException("participantListMessageListeners can not be null"); + } + + participantListMessageListeners.add(listener); + } + + public synchronized void removeListener(SignalingMessageReceiver.ParticipantListMessageListener listener) { + participantListMessageListeners.remove(listener); + } + + public synchronized void notifyUsersInRoom(List participants) { + for (SignalingMessageReceiver.ParticipantListMessageListener listener : new ArrayList<>(participantListMessageListeners)) { + listener.onUsersInRoom(participants); + } + } + + public synchronized void notifyParticipantsUpdate(List participants) { + for (SignalingMessageReceiver.ParticipantListMessageListener listener : new ArrayList<>(participantListMessageListeners)) { + listener.onParticipantsUpdate(participants); + } + } + + public synchronized void notifyAllParticipantsUpdate(long inCall) { + for (SignalingMessageReceiver.ParticipantListMessageListener listener : new ArrayList<>(participantListMessageListeners)) { + listener.onAllParticipantsUpdate(inCall); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java new file mode 100644 index 0000000..397ba7b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java @@ -0,0 +1,844 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter; +import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter; +import com.nextcloud.talk.models.json.participants.Participant; +import com.nextcloud.talk.models.json.signaling.NCIceCandidate; +import com.nextcloud.talk.models.json.signaling.NCMessagePayload; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; +import com.nextcloud.talk.models.json.websocket.CallWebSocketMessage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Hub to register listeners for signaling messages of different kinds. + *

+ * In general, if a listener is added while an event is being handled the new listener will not receive that event. + * An exception to that is adding a WebRtcMessageListener when handling an offer in an OfferMessageListener; in that + * case the "onOffer()" method of the WebRtcMessageListener will be called for that same offer. + *

+ * Similarly, if a listener is removed while an event is being handled the removed listener will still receive that + * event. Again the exception is removing a WebRtcMessageListener when handling an offer in an OfferMessageListener; in + * that case the "onOffer()" method of the WebRtcMessageListener will not be called for that offer. + *

+ * Adding and removing listeners, as well as notifying them is internally synchronized. This should be kept in mind + * if listeners are added or removed when handling an event to prevent deadlocks (nevertheless, just adding or + * removing a listener in the same thread handling the event is fine, and in most cases it will be fine too if done + * in a different thread, as long as the notifier thread is not forced to wait until the listener is added or removed). + *

+ * SignalingMessageReceiver does not fetch the signaling messages itself; subclasses must fetch them and then call + * the appropriate protected methods to process the messages and notify the listeners. + */ +public abstract class SignalingMessageReceiver { + + private final EnumActorTypeConverter enumActorTypeConverter = new EnumActorTypeConverter(); + + private final ParticipantListMessageNotifier participantListMessageNotifier = new ParticipantListMessageNotifier(); + + private final LocalParticipantMessageNotifier localParticipantMessageNotifier = new LocalParticipantMessageNotifier(); + + private final CallParticipantMessageNotifier callParticipantMessageNotifier = new CallParticipantMessageNotifier(); + + private final ConversationMessageNotifier conversationMessageNotifier = new ConversationMessageNotifier(); + + private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier(); + + private final WebRtcMessageNotifier webRtcMessageNotifier = new WebRtcMessageNotifier(); + + /** + * Listener for participant list messages. + *

+ * The messages are implicitly bound to the room currently joined in the signaling server; listeners are expected + * to know the current room. + */ + public interface ParticipantListMessageListener { + + /** + * List of all the participants in the room. + *

+ * This message is received only when the internal signaling server is used. + *

+ * The message is received periodically, and the participants may not have been modified since the last message. + *

+ * Only the following participant properties are set: + * - inCall + * - lastPing + * - sessionId + * - userId (if the participant is not a guest) + *

+ * "participantPermissions" is provided in the message (since Talk 13), but not currently set in the + * participant. "publishingPermissions" was provided instead in Talk 12, but it was not used anywhere, so it is + * ignored. + * + * @param participants all the participants (users and guests) in the room + */ + void onUsersInRoom(List participants); + + /** + * List of all the participants in the call or the room (depending on what triggered the event). + *

+ * This message is received only when the external signaling server is used. + *

+ * The message is received when any participant changed, although what changed is not provided and should be + * derived from the difference with previous messages. The list of participants may include only the + * participants in the call (including those that just left it and thus triggered the event) or all the + * participants currently in the room (participants in the room but not currently active, that is, without a + * session, are not included). + *

+ * Only the following participant properties are set: + * - inCall + * - lastPing + * - sessionId + * - type + * - userId (if the participant is not a guest) + *

+ * "nextcloudSessionId" is provided in the message (when the "inCall" property of any participant changed), but + * not currently set in the participant. + *

+ * "participantPermissions" is provided in the message (since Talk 13), but not currently set in the + * participant. "publishingPermissions" was provided instead in Talk 12, but it was not used anywhere, so it is + * ignored. + * + * @param participants all the participants (users and guests) in the room + */ + void onParticipantsUpdate(List participants); + + /** + * Update of the properties of all the participants in the room. + *

+ * This message is received only when the external signaling server is used. + * + * @param inCall the new value of the inCall property + */ + void onAllParticipantsUpdate(long inCall); + } + + /** + * Listener for local participant messages. + *

+ * The messages are implicitly bound to the local participant (or, rather, its session); listeners are expected + * to know the local participant. + *

+ * The messages are related to the conversation, so the local participant may or may not be in a call when they + * are received. + */ + public interface LocalParticipantMessageListener { + /** + * Request for the client to switch to the given conversation. + *

+ * This message is received only when the external signaling server is used. + * + * @param token the token of the conversation to switch to. + */ + void onSwitchTo(String token); + } + + /** + * Listener for call participant messages. + *

+ * The messages are bound to a specific call participant (or, rather, session), so each listener is expected to + * handle messages only for a single call participant. + *

+ * Although "unshareScreen" is technically bound to a specific peer connection it is instead treated as a general + * message on the call participant. + */ + public interface CallParticipantMessageListener { + void onRaiseHand(boolean state, long timestamp); + void onReaction(String reaction); + void onUnshareScreen(); + } + + /** + * Listener for conversation messages. + */ + public interface ConversationMessageListener { + void onStartTyping(String userId, String session); + void onStopTyping(String userId,String session); + } + + /** + * Listener for WebRTC offers. + *

+ * Unlike the WebRtcMessageListener, which is bound to a specific peer connection, an OfferMessageListener listens + * to all offer messages, no matter which peer connection they are bound to. This can be used, for example, to + * create a new peer connection when a remote offer for which there is no previous connection is received. + *

+ * When an offer is received all OfferMessageListeners are notified before any WebRtcMessageListener is notified. + */ + public interface OfferMessageListener { + void onOffer(String sessionId, String roomType, String sdp, String nick); + } + + /** + * Listener for WebRTC messages. + *

+ * The messages are bound to a specific peer connection, so each listener is expected to handle messages only for + * a single peer connection. + */ + public interface WebRtcMessageListener { + void onOffer(String sdp, String nick); + void onAnswer(String sdp, String nick); + void onCandidate(String sdpMid, int sdpMLineIndex, String sdp); + void onEndOfCandidates(); + } + + /** + * Adds a listener for participant list messages. + *

+ * A listener is expected to be added only once. If the same listener is added again it will be notified just once. + * + * @param listener the ParticipantListMessageListener + */ + public void addListener(ParticipantListMessageListener listener) { + participantListMessageNotifier.addListener(listener); + } + + public void removeListener(ParticipantListMessageListener listener) { + participantListMessageNotifier.removeListener(listener); + } + + /** + * Adds a listener for local participant messages. + *

+ * A listener is expected to be added only once. If the same listener is added again it will be notified just once. + * + * @param listener the LocalParticipantMessageListener + */ + public void addListener(LocalParticipantMessageListener listener) { + localParticipantMessageNotifier.addListener(listener); + } + + public void removeListener(LocalParticipantMessageListener listener) { + localParticipantMessageNotifier.removeListener(listener); + } + + /** + * Adds a listener for call participant messages. + *

+ * A listener is expected to be added only once. If the same listener is added again it will no longer be notified + * for the messages from the previous session ID. + * + * @param listener the CallParticipantMessageListener + * @param sessionId the ID of the session that messages come from + */ + public void addListener(CallParticipantMessageListener listener, String sessionId) { + callParticipantMessageNotifier.addListener(listener, sessionId); + } + + public void removeListener(CallParticipantMessageListener listener) { + callParticipantMessageNotifier.removeListener(listener); + } + + public void addListener(ConversationMessageListener listener) { + conversationMessageNotifier.addListener(listener); + } + + public void removeListener(ConversationMessageListener listener) { + conversationMessageNotifier.removeListener(listener); + } + + /** + * Adds a listener for all offer messages. + *

+ * A listener is expected to be added only once. If the same listener is added again it will be notified just once. + * + * @param listener the OfferMessageListener + */ + public void addListener(OfferMessageListener listener) { + offerMessageNotifier.addListener(listener); + } + + public void removeListener(OfferMessageListener listener) { + offerMessageNotifier.removeListener(listener); + } + + /** + * Adds a listener for WebRTC messages from the given session ID and room type. + *

+ * A listener is expected to be added only once. If the same listener is added again it will no longer be notified + * for the messages from the previous session ID or room type. + * + * @param listener the WebRtcMessageListener + * @param sessionId the ID of the session that messages come from + * @param roomType the room type that messages come from + */ + public void addListener(WebRtcMessageListener listener, String sessionId, String roomType) { + webRtcMessageNotifier.addListener(listener, sessionId, roomType); + } + + public void removeListener(WebRtcMessageListener listener) { + webRtcMessageNotifier.removeListener(listener); + } + + protected void processEvent(Map eventMap) { + if ("room".equals(eventMap.get("target")) && "switchto".equals(eventMap.get("type"))) { + processSwitchToEvent(eventMap); + + return; + } + + if ("participants".equals(eventMap.get("target")) && "update".equals(eventMap.get("type"))) { + processUpdateEvent(eventMap); + + return; + } + } + + private void processSwitchToEvent(Map eventMap) { + // Message schema: + // { + // "type": "event", + // "event": { + // "target": "room", + // "type": "switchto", + // "switchto": { + // "roomid": #STRING#, + // }, + // }, + // } + + Map switchToMap; + try { + switchToMap = (Map) eventMap.get("switchto"); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + + if (switchToMap == null) { + // Broken message, this should not happen. + return; + } + + String token; + try { + token = switchToMap.get("roomid").toString(); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + + localParticipantMessageNotifier.notifySwitchTo(token); + } + + private void processUpdateEvent(Map eventMap) { + Map updateMap; + try { + updateMap = (Map) eventMap.get("update"); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + + if (updateMap == null) { + // Broken message, this should not happen. + return; + } + + if (updateMap.get("all") != null && Boolean.parseBoolean(updateMap.get("all").toString())) { + processAllParticipantsUpdate(updateMap); + + return; + } + + if (updateMap.get("users") != null) { + processParticipantsUpdate(updateMap); + + return; + } + } + + private void processAllParticipantsUpdate(Map updateMap) { + // Message schema: + // { + // "type": "event", + // "event": { + // "target": "participants", + // "type": "update", + // "update": { + // "roomid": #STRING#, + // "incall": 0, + // "all": true, + // }, + // }, + // } + // + // Note that "incall" in participants->update is all in lower case when the message applies to all participants, + // even if it is "inCall" when the message provides separate properties for each participant. + + long inCall; + try { + inCall = Long.parseLong(updateMap.get("incall").toString()); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + + participantListMessageNotifier.notifyAllParticipantsUpdate(inCall); + } + + private void processParticipantsUpdate(Map updateMap) { + // Message schema: + // { + // "type": "event", + // "event": { + // "target": "participants", + // "type": "update", + // "update": { + // "roomid": #INTEGER#, + // "users": [ + // { + // "inCall": #INTEGER#, + // "lastPing": #INTEGER#, + // "sessionId": #STRING#, + // "participantType": #INTEGER#, + // "userId": #STRING#, // Optional + // "nextcloudSessionId": #STRING#, // Optional + // "internal": #BOOLEAN#, // Optional + // "participantPermissions": #INTEGER#, // Talk >= 13 + // "actorType": #STRING#, // Talk >= 20 + // "actorId": #STRING#, // Talk >= 20 + // }, + // ... + // ], + // }, + // }, + // } + // + // Note that "userId" in participants->update comes from the Nextcloud server, so it is "userId"; in other + // messages, like room->join, it comes directly from the external signaling server, so it is "userid" instead. + + List> users; + try { + users = (List>) updateMap.get("users"); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + + if (users == null) { + // Broken message, this should not happen. + return; + } + + List participants = new ArrayList<>(users.size()); + + for (Map user: users) { + try { + participants.add(getParticipantFromMessageMap(user)); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + } + + participantListMessageNotifier.notifyParticipantsUpdate(participants); + } + + protected void processUsersInRoom(List> users) { + // Message schema: + // { + // "type": "usersInRoom", + // "data": [ + // { + // "inCall": #INTEGER#, + // "lastPing": #INTEGER#, + // "roomId": #INTEGER#, + // "sessionId": #STRING#, + // "userId": #STRING#, // Always included, although it can be empty + // "participantPermissions": #INTEGER#, // Talk >= 13 + // "actorType": #STRING#, // Talk >= 20 + // "actorId": #STRING#, // Talk >= 20 + // }, + // ... + // ], + // } + + List participants = new ArrayList<>(users.size()); + + for (Map user: users) { + try { + participants.add(getParticipantFromMessageMap(user)); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + } + + participantListMessageNotifier.notifyUsersInRoom(participants); + } + + /** + * Creates and initializes a Participant from the data in the given map. + *

+ * Maps from internal and external signaling server messages can be used. Nevertheless, besides the differences + * between the messages and the optional properties, it is expected that the message is correct and the given data + * is parseable. Broken messages (for example, a string instead of an integer for "inCall" or a missing + * "sessionId") may cause a RuntimeException to be thrown. + * + * @param participantMap the map with the participant data + * @return the Participant + */ + private Participant getParticipantFromMessageMap(Map participantMap) { + Participant participant = new Participant(); + + participant.setInCall(Long.parseLong(participantMap.get("inCall").toString())); + participant.setLastPing(Long.parseLong(participantMap.get("lastPing").toString())); + participant.setSessionId(participantMap.get("sessionId").toString()); + + if (participantMap.get("userId") != null && !participantMap.get("userId").toString().isEmpty()) { + participant.setUserId(participantMap.get("userId").toString()); + } + + if (participantMap.get("internal") != null && Boolean.parseBoolean(participantMap.get("internal").toString())) { + participant.setInternal(Boolean.TRUE); + } + + if (participantMap.get("actorType") != null && !participantMap.get("actorType").toString().isEmpty()) { + participant.setActorType(enumActorTypeConverter.getFromString(participantMap.get("actorType").toString())); + } + + if (participantMap.get("actorId") != null && !participantMap.get("actorId").toString().isEmpty()) { + participant.setActorId(participantMap.get("actorId").toString()); + } + + // Only in external signaling messages + if (participantMap.get("participantType") != null) { + int participantTypeInt = Integer.parseInt(participantMap.get("participantType").toString()); + + EnumParticipantTypeConverter converter = new EnumParticipantTypeConverter(); + participant.setType(converter.getFromInt(participantTypeInt)); + } + + return participant; + } + + protected void processCallWebSocketMessage(CallWebSocketMessage callWebSocketMessage) { + + NCSignalingMessage signalingMessage = callWebSocketMessage.getNcSignalingMessage(); + + if (callWebSocketMessage.getSenderWebSocketMessage() != null && signalingMessage != null) { + String type = signalingMessage.getType(); + + String userId = callWebSocketMessage.getSenderWebSocketMessage().getUserid(); + String sessionId = signalingMessage.getFrom(); + + if ("startedTyping".equals(type)) { + conversationMessageNotifier.notifyStartTyping(userId, sessionId); + } + + if ("stoppedTyping".equals(type)) { + conversationMessageNotifier.notifyStopTyping(userId, sessionId); + } + } + } + + protected void processSignalingMessage(NCSignalingMessage signalingMessage) { + // Note that in the internal signaling server message "data" is the String representation of a JSON + // object, although it is already decoded when used here. + + String type = signalingMessage.getType(); + + String sessionId = signalingMessage.getFrom(); + String roomType = signalingMessage.getRoomType(); + + if ("raiseHand".equals(type)) { + // Message schema (external signaling server): + // { + // "type": "message", + // "message": { + // "sender": { + // ... + // }, + // "data": { + // "to": #STRING#, + // "sid": #STRING#, + // "roomType": "video", + // "type": "raiseHand", + // "payload": { + // "state": #BOOLEAN#, + // "timestamp": #LONG#, + // }, + // "from": #STRING#, + // }, + // }, + // } + // + // Message schema (internal signaling server): + // { + // "type": "message", + // "data": { + // "to": #STRING#, + // "sid": #STRING#, + // "roomType": "video", + // "type": "raiseHand", + // "payload": { + // "state": #BOOLEAN#, + // "timestamp": #LONG#, + // }, + // "from": #STRING#, + // }, + // } + + NCMessagePayload payload = signalingMessage.getPayload(); + if (payload == null) { + // Broken message, this should not happen. + return; + } + + Boolean state = payload.getState(); + Long timestamp = payload.getTimestamp(); + + if (state == null || timestamp == null) { + // Broken message, this should not happen. + return; + } + + callParticipantMessageNotifier.notifyRaiseHand(sessionId, state, timestamp); + + return; + } + + if ("reaction".equals(type)) { + // Message schema (external signaling server): + // { + // "type": "message", + // "message": { + // "sender": { + // ... + // }, + // "data": { + // "to": #STRING#, + // "roomType": "video", + // "type": "reaction", + // "payload": { + // "reaction": #STRING#, + // }, + // "from": #STRING#, + // }, + // }, + // } + // + // Message schema (internal signaling server): + // { + // "type": "message", + // "data": { + // "to": #STRING#, + // "roomType": "video", + // "type": "reaction", + // "payload": { + // "reaction": #STRING#, + // }, + // "from": #STRING#, + // }, + // } + + NCMessagePayload payload = signalingMessage.getPayload(); + if (payload == null) { + // Broken message, this should not happen. + return; + } + + String reaction = payload.getReaction(); + if (reaction == null) { + // Broken message, this should not happen. + return; + } + + callParticipantMessageNotifier.notifyReaction(sessionId, reaction); + + return; + } + + // "unshareScreen" messages are directly sent to the screen peer connection when the internal signaling + // server is used, and to the room when the external signaling server is used. However, the (relevant) data + // of the received message ("from" and "type") is the same in both cases. + if ("unshareScreen".equals(type)) { + // Message schema (external signaling server): + // { + // "type": "message", + // "message": { + // "sender": { + // ... + // }, + // "data": { + // "roomType": "screen", + // "type": "unshareScreen", + // "from": #STRING#, + // }, + // }, + // } + // + // Message schema (internal signaling server): + // { + // "type": "message", + // "data": { + // "to": #STRING#, + // "sid": #STRING#, + // "broadcaster": #STRING#, + // "roomType": "screen", + // "type": "unshareScreen", + // "from": #STRING#, + // }, + // } + + callParticipantMessageNotifier.notifyUnshareScreen(sessionId); + + return; + } + + if ("offer".equals(type)) { + // Message schema (external signaling server): + // { + // "type": "message", + // "message": { + // "sender": { + // ... + // }, + // "data": { + // "to": #STRING#, + // "from": #STRING#, + // "type": "offer", + // "roomType": #STRING#, // "video" or "screen" + // "payload": { + // "type": "offer", + // "sdp": #STRING#, + // }, + // "sid": #STRING#, // external signaling server >= 0.5.0 + // }, + // }, + // } + // + // Message schema (internal signaling server): + // { + // "type": "message", + // "data": { + // "to": #STRING#, + // "sid": #STRING#, + // "roomType": #STRING#, // "video" or "screen" + // "type": "offer", + // "payload": { + // "type": "offer", + // "sdp": #STRING#, + // "nick": #STRING#, // Optional + // }, + // "from": #STRING#, + // }, + // } + + NCMessagePayload payload = signalingMessage.getPayload(); + if (payload == null) { + // Broken message, this should not happen. + return; + } + + String sdp = payload.getSdp(); + String nick = payload.getNick(); + + // If "processSignalingMessage" is called with two offers from two different threads it is possible, + // although extremely unlikely, that the WebRtcMessageListeners for the second offer are notified before the + // WebRtcMessageListeners for the first offer. This should not be a problem, though, so for simplicity + // the statements are not synchronized. + offerMessageNotifier.notifyOffer(sessionId, roomType, sdp, nick); + webRtcMessageNotifier.notifyOffer(sessionId, roomType, sdp, nick); + + return; + } + + if ("answer".equals(type)) { + // Message schema: same as offers, but with type "answer". + + NCMessagePayload payload = signalingMessage.getPayload(); + if (payload == null) { + // Broken message, this should not happen. + return; + } + + String sdp = payload.getSdp(); + String nick = payload.getNick(); + + webRtcMessageNotifier.notifyAnswer(sessionId, roomType, sdp, nick); + + return; + } + + if ("candidate".equals(type)) { + // Message schema (external signaling server): + // { + // "type": "message", + // "message": { + // "sender": { + // ... + // }, + // "data": { + // "to": #STRING#, + // "from": #STRING#, + // "type": "candidate", + // "roomType": #STRING#, // "video" or "screen" + // "payload": { + // "candidate": { + // "candidate": #STRING#, + // "sdpMid": #STRING#, + // "sdpMLineIndex": #INTEGER#, + // }, + // }, + // "sid": #STRING#, // external signaling server >= 0.5.0 + // }, + // }, + // } + // + // Message schema (internal signaling server): + // { + // "type": "message", + // "data": { + // "to": #STRING#, + // "sid": #STRING#, + // "roomType": #STRING#, // "video" or "screen" + // "type": "candidate", + // "payload": { + // "candidate": { + // "candidate": #STRING#, + // "sdpMid": #STRING#, + // "sdpMLineIndex": #INTEGER#, + // }, + // }, + // "from": #STRING#, + // }, + // } + + NCMessagePayload payload = signalingMessage.getPayload(); + if (payload == null) { + // Broken message, this should not happen. + return; + } + + NCIceCandidate ncIceCandidate = payload.getIceCandidate(); + if (ncIceCandidate == null) { + // Broken message, this should not happen. + return; + } + + webRtcMessageNotifier.notifyCandidate(sessionId, + roomType, + ncIceCandidate.getSdpMid(), + ncIceCandidate.getSdpMLineIndex(), + ncIceCandidate.getCandidate()); + + return; + } + + if ("endOfCandidates".equals(type)) { + webRtcMessageNotifier.notifyEndOfCandidates(sessionId, roomType); + + return; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageSender.java b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageSender.java new file mode 100644 index 0000000..5645216 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageSender.java @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; + +/** + * Interface to send signaling messages. + */ +public interface SignalingMessageSender { + + /** + * Sends the given signaling message. + * + * @param ncSignalingMessage the message to send + */ + void send(NCSignalingMessage ncSignalingMessage); + +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/WebRtcMessageNotifier.java b/app/src/main/java/com/nextcloud/talk/signaling/WebRtcMessageNotifier.java new file mode 100644 index 0000000..4bc6f3c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/WebRtcMessageNotifier.java @@ -0,0 +1,107 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Helper class to register and notify WebRtcMessageListeners. + *

+ * This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against + * a SignalingMessageReceiver rather than against a WebRtcMessageNotifier. + */ +class WebRtcMessageNotifier { + + private final List webRtcMessageListenersFrom = new ArrayList<>(); + + /** + * Helper class to associate a WebRtcMessageListener with a session ID and room type. + */ + private static class WebRtcMessageListenerFrom { + public final SignalingMessageReceiver.WebRtcMessageListener listener; + public final String sessionId; + public final String roomType; + + private WebRtcMessageListenerFrom(SignalingMessageReceiver.WebRtcMessageListener listener, + String sessionId, + String roomType) { + this.listener = listener; + this.sessionId = sessionId; + this.roomType = roomType; + } + } + + public synchronized void addListener(SignalingMessageReceiver.WebRtcMessageListener listener, String sessionId, String roomType) { + if (listener == null) { + throw new IllegalArgumentException("WebRtcMessageListener can not be null"); + } + + if (sessionId == null) { + throw new IllegalArgumentException("sessionId can not be null"); + } + + if (roomType == null) { + throw new IllegalArgumentException("roomType can not be null"); + } + + removeListener(listener); + + webRtcMessageListenersFrom.add(new WebRtcMessageListenerFrom(listener, sessionId, roomType)); + } + + public synchronized void removeListener(SignalingMessageReceiver.WebRtcMessageListener listener) { + Iterator it = webRtcMessageListenersFrom.iterator(); + while (it.hasNext()) { + WebRtcMessageListenerFrom listenerFrom = it.next(); + + if (listenerFrom.listener == listener) { + it.remove(); + + return; + } + } + } + + private List getListenersFor(String sessionId, String roomType) { + List webRtcMessageListeners = + new ArrayList<>(webRtcMessageListenersFrom.size()); + + for (WebRtcMessageListenerFrom listenerFrom : webRtcMessageListenersFrom) { + if (listenerFrom.sessionId.equals(sessionId) && listenerFrom.roomType.equals(roomType)) { + webRtcMessageListeners.add(listenerFrom.listener); + } + } + + return webRtcMessageListeners; + } + + public synchronized void notifyOffer(String sessionId, String roomType, String sdp, String nick) { + for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) { + listener.onOffer(sdp, nick); + } + } + + public synchronized void notifyAnswer(String sessionId, String roomType, String sdp, String nick) { + for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) { + listener.onAnswer(sdp, nick); + } + } + + public synchronized void notifyCandidate(String sessionId, String roomType, String sdpMid, int sdpMLineIndex, String sdp) { + for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) { + listener.onCandidate(sdpMid, sdpMLineIndex, sdp); + } + } + + public synchronized void notifyEndOfCandidates(String sessionId, String roomType) { + for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) { + listener.onEndOfCandidates(); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt b/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt new file mode 100644 index 0000000..d9492b7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/threadsoverview/ThreadsOverviewActivity.kt @@ -0,0 +1,257 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.threadsoverview + +import android.content.Intent +import android.os.Bundle +import android.text.format.DateUtils +import android.util.Log +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.ChatActivity.Companion.TAG +import com.nextcloud.talk.components.ColoredStatusBar +import com.nextcloud.talk.components.StandardAppBar +import com.nextcloud.talk.data.database.mappers.asModel +import com.nextcloud.talk.models.json.threads.ThreadInfo +import com.nextcloud.talk.threadsoverview.components.ThreadRow +import com.nextcloud.talk.threadsoverview.viewmodels.ThreadsOverviewViewModel +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_THREAD_ID +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ThreadsOverviewActivity : BaseActivity() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userManager: UserManager + + lateinit var threadsOverviewViewModel: ThreadsOverviewViewModel + + var threadsSourceUrl: String = "" + var appbarTitle: String = "" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + threadsOverviewViewModel = ViewModelProvider( + this, + viewModelFactory + )[ThreadsOverviewViewModel::class.java] + + val colorScheme = viewThemeUtils.getColorScheme(this) + + val extras: Bundle? = intent.extras + threadsSourceUrl = extras?.getString(KEY_THREADS_SOURCE_URL).orEmpty() + appbarTitle = extras?.getString(KEY_APPBAR_TITLE).orEmpty() + + setContent { + val backgroundColor = colorResource(id = R.color.bg_default) + + MaterialTheme( + colorScheme = colorScheme + ) { + ColoredStatusBar() + Scaffold( + modifier = Modifier + .statusBarsPadding(), + topBar = { + StandardAppBar( + title = appbarTitle, + null + ) + }, + content = { paddingValues -> + val uiState by threadsOverviewViewModel.threadsListState.collectAsState() + + Column( + Modifier + .padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp) + .background(backgroundColor) + .fillMaxSize() + ) { + ThreadsOverviewScreen( + uiState, + onThreadClick = { roomToken, threadId -> + navigateToChatActivity(roomToken, threadId) + } + ) + } + } + ) + } + } + } + + private fun navigateToChatActivity(roomToken: String, threadId: Int) { + val bundle = Bundle() + bundle.putString(KEY_ROOM_TOKEN, roomToken) + bundle.putLong(KEY_THREAD_ID, threadId.toLong()) + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + startActivity(chatIntent) + } + + override fun onResume() { + super.onResume() + supportActionBar?.show() + threadsOverviewViewModel.init(threadsSourceUrl) + } + + companion object { + val TAG = ThreadsOverviewActivity::class.java.simpleName + val KEY_APPBAR_TITLE = "KEY_APPBAR_TITLE" + val KEY_THREADS_SOURCE_URL = "KEY_THREADS_SOURCE_URL" + } +} + +@Composable +fun ThreadsOverviewScreen( + uiState: ThreadsOverviewViewModel.ThreadsListUiState, + onThreadClick: (roomToken: String, threadId: Int) -> Unit +) { + when (val state = uiState) { + is ThreadsOverviewViewModel.ThreadsListUiState.None -> { + LoadingIndicator() + } + + is ThreadsOverviewViewModel.ThreadsListUiState.Success -> { + ThreadsList( + threads = state.threadsList!!, + onThreadClick = onThreadClick + ) + } + + is ThreadsOverviewViewModel.ThreadsListUiState.Error -> { + Log.e(TAG, "Error when retrieving threads", uiState.exception) + ErrorView(message = stringResource(R.string.nc_common_error_sorry)) + } + } +} + +@Composable +fun ThreadsList(threads: List, onThreadClick: (roomToken: String, threadId: Int) -> Unit) { + val space = ' ' + if (threads.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text(stringResource(R.string.threads_list_empty)) + } + return + } + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items( + items = threads, + key = { threadInfo -> threadInfo.thread!!.id } + ) { threadInfo -> + val messageJson = threadInfo.last ?: threadInfo.first + val messageModel = messageJson?.asModel() + + ThreadRow( + roomToken = threadInfo.thread!!.roomToken, + threadId = threadInfo.thread!!.id, + title = threadInfo.thread?.title.orEmpty(), + numReplies = pluralStringResource( + R.plurals.thread_replies, + threadInfo.thread?.numReplies ?: 0, + threadInfo.thread?.numReplies ?: 0 + ), + secondLineTitle = messageModel?.actorDisplayName?.substringBefore(space)?.let { "$it:" }.orEmpty(), + secondLine = messageModel?.text.orEmpty(), + date = getLastActivityDate(threadInfo), // replace with value from api when available + onClick = onThreadClick + ) + } + item { + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@Suppress("MagicNumber") +private fun getLastActivityDate(threadInfo: ThreadInfo): String { + val oneSecond = 1000L + + val lastActivityTimestamp = threadInfo.thread?.lastActivity ?: 0 + + val lastActivityDate = DateUtils.getRelativeTimeSpanString( + lastActivityTimestamp.times(oneSecond), + System.currentTimeMillis(), + 0, + DateUtils.FORMAT_ABBREV_RELATIVE + ).toString() + return lastActivityDate +} + +@Composable +fun LoadingIndicator() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} + +@Composable +fun ErrorView(message: String) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text(text = message, color = MaterialTheme.colorScheme.error) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/threadsoverview/components/ThreadRow.kt b/app/src/main/java/com/nextcloud/talk/threadsoverview/components/ThreadRow.kt new file mode 100644 index 0000000..5498190 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/threadsoverview/components/ThreadRow.kt @@ -0,0 +1,213 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.threadsoverview.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.R +import com.nextcloud.talk.utils.ColorGenerator + +@Suppress("LongParameterList", "Detekt.LongMethod") +@Composable +fun ThreadRow( + roomToken: String, + threadId: Int, + title: String, + secondLineTitle: String, + secondLine: String, + numReplies: String, + date: String, + onClick: ((String, Int) -> Unit?)? +) { + Row( + modifier = Modifier.Companion + .fillMaxWidth() + .clickable(enabled = onClick != null) { + onClick?.invoke(roomToken, threadId) + } + .padding(vertical = 8.dp, horizontal = 8.dp), + verticalAlignment = Alignment.Companion.CenterVertically + ) { + ThreadsIcon(title) + + Spacer(modifier = Modifier.Companion.width(12.dp)) + + Column { + Row { + Text( + modifier = Modifier.Companion.weight(1f), + text = title, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Companion.Ellipsis + ) + Spacer(modifier = Modifier.Companion.width(4.dp)) + Text( + text = numReplies, + style = MaterialTheme.typography.titleSmall + ) + } + + Spacer(modifier = Modifier.Companion.height(2.dp)) + + Row( + verticalAlignment = Alignment.Companion.CenterVertically + ) { + Text( + text = secondLineTitle, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Normal, + maxLines = 1, + overflow = TextOverflow.Companion.Ellipsis + ) + Spacer(modifier = Modifier.Companion.width(4.dp)) + Text( + modifier = Modifier.Companion.weight(1f), + text = secondLine, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Normal, + maxLines = 1, + overflow = TextOverflow.Companion.Ellipsis + ) + Spacer(modifier = Modifier.Companion.width(4.dp)) + Text( + text = date, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.Companion.width(16.dp)) + } +} + +@Composable +fun ThreadsIcon(title: String) { + val baseColorInt = ColorGenerator.usernameToColor(title) + + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(Color(baseColorInt).copy(alpha = 0.1f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.outline_forum_24), + contentDescription = null, + tint = Color(baseColorInt).copy(alpha = 0.9f), + modifier = Modifier.size(32.dp) + ) + } +} + +@Preview +@Composable +fun ThreadRowPreview() { + ThreadRow( + roomToken = "1234", + threadId = 123, + title = "title1", + secondLine = "last message", + secondLineTitle = "Mia:", + numReplies = "12 replies", + date = "14 sec ago", + onClick = null + ) +} + +@Preview( + name = "Dark Mode", + uiMode = Configuration.UI_MODE_NIGHT_YES +) +@Composable +fun ThreadRowPreviewDark() { + ThreadRow( + roomToken = "1234", + threadId = 123, + title = "title2", + secondLine = "last message", + secondLineTitle = "Mia:", + numReplies = "12 replies", + date = "14 sec ago", + onClick = null + ) +} + +@Preview +@Composable +fun ThreadRowUnreadMessagePreview() { + ThreadRow( + roomToken = "1234", + threadId = 123, + title = "title3", + secondLine = "last message", + secondLineTitle = "Mia:", + numReplies = "12 replies", + date = "14 sec ago", + onClick = null + ) +} + +@Preview +@Composable +fun ThreadRowMentionPreview() { + ThreadRow( + roomToken = "1234", + threadId = 123, + title = "title3", + secondLine = "last message", + secondLineTitle = "Mia:", + numReplies = "12 replies", + date = "14 sec ago", + onClick = null + ) +} + +@Preview +@Composable +fun ThreadRowDirectMentionPreview() { + ThreadRow( + roomToken = "1234", + threadId = 123, + title = "title with a verrrrrrrrrrrrrrrrrrrrrrrrry long text", + secondLine = "title with a verrrrrrrrrrrrrrrrrrrrrrrrry long text", + secondLineTitle = "Mia:", + numReplies = "12 replies", + date = "14 sec ago", + onClick = null + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/threadsoverview/data/ThreadsRepository.kt b/app/src/main/java/com/nextcloud/talk/threadsoverview/data/ThreadsRepository.kt new file mode 100644 index 0000000..7051a85 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/threadsoverview/data/ThreadsRepository.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.threadsoverview.data + +import com.nextcloud.talk.models.json.threads.ThreadOverall +import com.nextcloud.talk.models.json.threads.ThreadsOverall + +interface ThreadsRepository { + + suspend fun getThreads(credentials: String, url: String, limit: Int?): ThreadsOverall + + suspend fun getThread(credentials: String, url: String): ThreadOverall + + suspend fun setThreadNotificationLevel(credentials: String, url: String, level: Int): ThreadOverall +} diff --git a/app/src/main/java/com/nextcloud/talk/threadsoverview/data/ThreadsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/threadsoverview/data/ThreadsRepositoryImpl.kt new file mode 100644 index 0000000..9afc4d6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/threadsoverview/data/ThreadsRepositoryImpl.kt @@ -0,0 +1,36 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.threadsoverview.data + +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.threads.ThreadOverall +import com.nextcloud.talk.models.json.threads.ThreadsOverall +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import javax.inject.Inject + +class ThreadsRepositoryImpl @Inject constructor( + private val ncApiCoroutines: NcApiCoroutines, + userProvider: CurrentUserProviderNew +) : ThreadsRepository { + + val currentUser: User = userProvider.currentUser.blockingGet() + + override suspend fun getThreads(credentials: String, url: String, limit: Int?): ThreadsOverall = + ncApiCoroutines.getThreads(credentials, url, limit) + + override suspend fun getThread(credentials: String, url: String): ThreadOverall = + ncApiCoroutines.getThread(credentials, url) + + override suspend fun setThreadNotificationLevel(credentials: String, url: String, level: Int): ThreadOverall = + ncApiCoroutines.setThreadNotificationLevel(credentials, url, level) + + companion object { + val TAG = ThreadsRepositoryImpl::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/threadsoverview/viewmodels/ThreadsOverviewViewModel.kt b/app/src/main/java/com/nextcloud/talk/threadsoverview/viewmodels/ThreadsOverviewViewModel.kt new file mode 100644 index 0000000..eabcec6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/threadsoverview/viewmodels/ThreadsOverviewViewModel.kt @@ -0,0 +1,80 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.threadsoverview.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel.Companion.FOLLOWED_THREADS_EXIST +import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel.Companion.FOLLOWED_THREADS_EXIST_LAST_CHECK +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.threads.ThreadInfo +import com.nextcloud.talk.models.json.threads.ThreadsOverall +import com.nextcloud.talk.threadsoverview.data.ThreadsRepository +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.UserIdUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("TooGenericExceptionCaught") +class ThreadsOverviewViewModel @Inject constructor( + private val threadsRepository: ThreadsRepository, + private val currentUserProvider: CurrentUserProviderNew, + private val arbitraryStorageManager: ArbitraryStorageManager +) : ViewModel() { + private val _currentUser = currentUserProvider.currentUser.blockingGet() + val currentUser: User = _currentUser + val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token) ?: "" + + private val _threadsListState = MutableStateFlow(ThreadsListUiState.None) + val threadsListState: StateFlow = _threadsListState + + fun init(url: String) { + getThreads(credentials, url) + } + + fun getThreads(credentials: String, url: String) { + viewModelScope.launch { + try { + val threads = threadsRepository.getThreads(credentials, url, null) + _threadsListState.value = ThreadsListUiState.Success(threads.ocs?.data) + updateFollowedThreadsIndicator(url, threads) + } catch (exception: Exception) { + _threadsListState.value = ThreadsListUiState.Error(exception) + } + } + } + + private fun updateFollowedThreadsIndicator(url: String, threads: ThreadsOverall) { + val subscribedThreadsEndpoint = "subscribed-threads" + if (url.contains(subscribedThreadsEndpoint) && threads.ocs?.data?.isEmpty() == true) { + val accountId = UserIdUtils.getIdForUser(currentUserProvider.currentUser.blockingGet()) + arbitraryStorageManager.storeStorageSetting( + accountId, + FOLLOWED_THREADS_EXIST, + false.toString(), + "" + ) + arbitraryStorageManager.storeStorageSetting( + accountId, + FOLLOWED_THREADS_EXIST_LAST_CHECK, + null, + "" + ) + } + } + + sealed class ThreadsListUiState { + data object None : ThreadsListUiState() + data class Success(val threadsList: List?) : ThreadsListUiState() + data class Error(val exception: Exception) : ThreadsListUiState() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepository.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepository.kt new file mode 100644 index 0000000..4a23477 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepository.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023-2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.repositories + +import com.nextcloud.talk.translate.repositories.model.Language +import io.reactivex.Observable + +interface TranslateRepository { + + fun translateMessage( + authorization: String, + url: String, + text: String, + toLanguage: String, + fromLanguage: String? + ): Observable + + fun getLanguages(authorization: String, url: String): Observable> +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepositoryImpl.kt new file mode 100644 index 0000000..efb958a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/TranslateRepositoryImpl.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus1 + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.repositories + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.translate.repositories.model.Language +import io.reactivex.Observable +import javax.inject.Inject + +class TranslateRepositoryImpl @Inject constructor(private val ncApi: NcApi) : TranslateRepository { + + override fun translateMessage( + authorization: String, + url: String, + text: String, + toLanguage: String, + fromLanguage: String? + ): Observable = + ncApi.translateMessage(authorization, url, text, toLanguage, fromLanguage).map { + it.ocs?.data!!.text + } + + override fun getLanguages(authorization: String, url: String): Observable> = + ncApi.getLanguages(authorization, url).map { + it.ocs?.data?.languages + } +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/Language.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/Language.kt new file mode 100644 index 0000000..55e6b3e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/Language.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Language( + @JsonField(name = ["from"]) + var from: String?, + @JsonField(name = ["fromLabel"]) + var fromLabel: String?, + @JsonField(name = ["to"]) + var to: String?, + @JsonField(name = ["toLabel"]) + var toLabel: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesData.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesData.kt new file mode 100644 index 0000000..e0f9a8f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesData.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class LanguagesData( + @JsonField(name = ["languageDetection"]) + var languageDetection: Boolean?, + @JsonField(name = ["languages"]) + var languages: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOCS.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOCS.kt new file mode 100644 index 0000000..d8f7fa4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOCS.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class LanguagesOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: LanguagesData? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOverall.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOverall.kt new file mode 100644 index 0000000..2c57137 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/LanguagesOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class LanguagesOverall( + @JsonField(name = ["ocs"]) + var ocs: LanguagesOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateData.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateData.kt new file mode 100644 index 0000000..b10e9d7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateData.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TranslateData( + @JsonField(name = ["text"]) + var text: String?, + @JsonField(name = ["from"]) + var fromLanguage: String? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateOCS.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateOCS.kt new file mode 100644 index 0000000..02484c1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslateOCS.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class TranslateOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: TranslateData? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, TranslateData()) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslationsOverall.kt b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslationsOverall.kt new file mode 100644 index 0000000..deadfc6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/repositories/model/TranslationsOverall.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.repositories.model + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +class TranslationsOverall( + @JsonField(name = ["ocs"]) + var ocs: TranslateOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt b/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt new file mode 100644 index 0000000..8817d7c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/ui/TranslateActivity.kt @@ -0,0 +1,290 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.ui + +import android.app.AlertDialog +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.text.method.ScrollingMovementMethod +import android.util.Log +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import androidx.core.graphics.drawable.toDrawable +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.ActivityTranslateBinding +import com.nextcloud.talk.translate.repositories.model.Language +import com.nextcloud.talk.translate.viewmodels.TranslateViewModel +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.bundle.BundleKeys +import java.util.Locale +import javax.inject.Inject + +@Suppress("TooManyFunctions") +@AutoInjector(NextcloudTalkApplication::class) +class TranslateActivity : BaseActivity() { + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + lateinit var viewModel: TranslateViewModel + lateinit var binding: ActivityTranslateBinding + + private var toLanguages: Array? = null + private var fromLanguages: Array? = null + private var languages: List? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + binding = ActivityTranslateBinding.inflate(layoutInflater) + viewModel = ViewModelProvider(this, viewModelFactory)[TranslateViewModel::class.java] + + viewModel.viewState.observe(this) { state -> + when (state) { + is TranslateViewModel.StartState -> { + onStartState() + } + + is TranslateViewModel.TranslatedState -> { + onTranslatedState(state.msg) + } + + is TranslateViewModel.TranslationErrorState -> { + onTranslationErrorState() + } + + is TranslateViewModel.LanguagesErrorState -> { + onLanguagesErrorState() + } + + is TranslateViewModel.LanguagesRetrievedState -> { + Log.d(TAG, "Languages are: ${state.list}") + languages = state.list + getLanguageOptions() + setupSpinners() + setItems() + } + } + } + setupActionBar() + setContentView(binding.root) + initSystemBars() + setupTextViews() + viewModel.getLanguages() + setupCopyButton() + + if (savedInstanceState == null) { + val text = intent.extras!!.getString(BundleKeys.KEY_TRANSLATE_MESSAGE) + viewModel.translateMessage(Locale.getDefault().language, null, text!!) + } else { + binding.translatedMessageTextview.text = savedInstanceState.getString(BundleKeys.SAVED_TRANSLATED_MESSAGE) + } + } + + override fun onResume() { + super.onResume() + languages?.let { setItems() } + } + override fun onSaveInstanceState(outState: Bundle) { + outState.run { + putString(BundleKeys.SAVED_TRANSLATED_MESSAGE, binding.translatedMessageTextview.text.toString()) + } + super.onSaveInstanceState(outState) + } + + private fun setupCopyButton() { + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.copyTranslatedMessage) + binding.copyTranslatedMessage.setOnClickListener { + val clipboardManager = + getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText( + resources?.getString(R.string.nc_app_product_name), + binding.translatedMessageTextview.text?.toString() + ) + clipboardManager.setPrimaryClip(clipData) + } + } + + private fun setupActionBar() { + setSupportActionBar(binding.translationToolbar) + binding.translationToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(resources!!.getColor(R.color.transparent, null).toDrawable()) + supportActionBar?.title = resources!!.getString(R.string.translation) + viewThemeUtils.material.themeToolbar(binding.translationToolbar) + } + + private fun setupTextViews() { + viewThemeUtils.talk.themeIncomingMessageBubble( + binding.originalMessageTextview, + grouped = true, + deleted = false + ) + viewThemeUtils.talk.themeIncomingMessageBubble( + binding.translatedMessageTextview, + grouped = true, + deleted = false + ) + + binding.originalMessageTextview.movementMethod = ScrollingMovementMethod() + binding.translatedMessageTextview.movementMethod = ScrollingMovementMethod() + val text = intent.extras!!.getString(BundleKeys.KEY_TRANSLATE_MESSAGE) + binding.originalMessageTextview.text = text + } + + private fun getLanguageOptions() { + val fromLanguagesSet = mutableSetOf(resources.getString(R.string.translation_detect_language)) + val toLanguagesSet = mutableSetOf(resources.getString(R.string.translation_device_settings)) + + for (language in languages!!) { + val locale = Locale.getDefault().language + if (language.from != locale) { + toLanguagesSet.add(language.fromLabel!!) + } + + fromLanguagesSet.add(language.toLabel!!) + } + + toLanguages = toLanguagesSet.toTypedArray() + fromLanguages = fromLanguagesSet.toTypedArray() + } + + private fun enableSpinners(value: Boolean) { + binding.fromLanguageInputLayout.isEnabled = value + binding.toLanguageInputLayout.isEnabled = value + } + + private fun showDialog(titleInt: Int, messageInt: Int) { + val dialogBuilder = MaterialAlertDialogBuilder(this@TranslateActivity) + .setIcon( + viewThemeUtils.dialog.colorMaterialAlertDialogIcon( + context, + R.drawable.ic_warning_white + ) + ) + .setTitle(titleInt) + .setMessage(messageInt) + .setPositiveButton(R.string.nc_ok) { dialog, _ -> + dialog.dismiss() + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(context, dialogBuilder) + + val dialog = dialogBuilder.show() + + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE) + ) + } + + private fun getISOFromLanguage(language: String): String { + if (resources.getString(R.string.translation_device_settings) == language) { + return Locale.getDefault().language + } + + return getISOFromServer(language) + } + + private fun getISOFromServer(label: String): String { + var result = "" + for (language in languages!!) { + if (language.fromLabel == label) { + result = language.from!! + } + } + + return result + } + + private fun setupSpinners() { + viewThemeUtils.material.colorTextInputLayout(binding.fromLanguageInputLayout) + viewThemeUtils.material.colorTextInputLayout(binding.toLanguageInputLayout) + fillSpinners() + val text = intent.extras!!.getString(BundleKeys.KEY_TRANSLATE_MESSAGE) + + binding.fromLanguage.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ -> + val fromLabel: String = getISOFromLanguage(parent.getItemAtPosition(position).toString()) + val toLabel: String = getISOFromLanguage(binding.toLanguage.text.toString()) + viewModel.translateMessage(toLabel, fromLabel, text!!) + } + + binding.toLanguage.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ -> + val toLabel: String = getISOFromLanguage(parent.getItemAtPosition(position).toString()) + val fromLabel: String = getISOFromLanguage(binding.fromLanguage.text.toString()) + viewModel.translateMessage(toLabel, fromLabel, text!!) + } + } + + private fun fillSpinners() { + binding.fromLanguage.setAdapter( + ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, fromLanguages!!) + ) + if (fromLanguages!!.isNotEmpty()) { + binding.fromLanguage.setText(fromLanguages!![0]) + } + + binding.toLanguage.setAdapter( + ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, toLanguages!!) + ) + if (toLanguages!!.isNotEmpty()) { + binding.toLanguage.setText(toLanguages!![0]) + } + } + + private fun setItems() { + binding.fromLanguage.setSimpleItems(fromLanguages!!) + binding.toLanguage.setSimpleItems(toLanguages!!) + } + + private fun onStartState() { + enableSpinners(false) + binding.translatedMessageContainer.visibility = View.GONE + binding.progressBar.visibility = View.VISIBLE + binding.copyTranslatedMessage.visibility = View.GONE + } + + private fun onTranslatedState(msg: String) { + binding.progressBar.visibility = View.GONE + binding.translatedMessageContainer.visibility = View.VISIBLE + binding.translatedMessageTextview.text = msg + binding.copyTranslatedMessage.visibility = View.VISIBLE + enableSpinners(true) + } + + private fun onTranslationErrorState() { + binding.progressBar.visibility = View.GONE + enableSpinners(true) + showDialog(R.string.translation_error_title, R.string.translation_error_message) + } + + private fun onLanguagesErrorState() { + binding.progressBar.visibility = View.GONE + enableSpinners(true) + showDialog(R.string.languages_error_title, R.string.languages_error_message) + } + + companion object { + val TAG = TranslateActivity::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt b/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt new file mode 100644 index 0000000..8cd7535 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/translate/viewmodels/TranslateViewModel.kt @@ -0,0 +1,119 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Julius Linus1 + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.translate.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.translate.repositories.TranslateRepository +import com.nextcloud.talk.translate.repositories.model.Language +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class TranslateViewModel @Inject constructor( + private val repository: TranslateRepository, + private val currentUserProvider: CurrentUserProviderNew +) : ViewModel() { + + sealed interface ViewState + + data object StartState : ViewState + class TranslatedState(val msg: String) : ViewState + + class LanguagesRetrievedState(val list: List) : ViewState + + data object LanguagesErrorState : ViewState + + data object TranslationErrorState : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(StartState) + val viewState: LiveData + get() = _viewState + + fun translateMessage(toLanguage: String, fromLanguage: String?, text: String) { + val currentUser: User = currentUserProvider.currentUser.blockingGet() + val authorization: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!! + val url: String = ApiUtils.getUrlForTranslation(currentUser.baseUrl!!) + val calculatedFromLanguage = + if (fromLanguage == null || fromLanguage == "") { + null + } else { + fromLanguage + } + Log.i(TAG, "translateMessage Called") + repository.translateMessage( + authorization, + url, + text, + toLanguage, + calculatedFromLanguage + ) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(TranslateObserver()) + } + + fun getLanguages() { + val currentUser: User = currentUserProvider.currentUser.blockingGet() + val authorization: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!! + val url: String = ApiUtils.getUrlForLanguages(currentUser.baseUrl!!) + Log.d(TAG, "URL is: $url") + repository.getLanguages(authorization, url) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer> { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onError(e: Throwable) { + _viewState.value = LanguagesErrorState + Log.e(TAG, "Error while retrieving languages: $e") + } + + override fun onComplete() { + // unused atm + } + + override fun onNext(list: List) { + _viewState.value = LanguagesRetrievedState(list) + Log.d(TAG, "Languages retrieved: $list") + } + }) + } + + inner class TranslateObserver : Observer { + override fun onSubscribe(d: Disposable) { + _viewState.value = StartState + } + + override fun onNext(translatedMessage: String) { + _viewState.value = TranslatedState(translatedMessage) + } + + override fun onError(e: Throwable) { + _viewState.value = TranslationErrorState + Log.e(TAG, "Error while translating message", e) + } + + override fun onComplete() { + // nothing? + } + } + companion object { + private val TAG = TranslateViewModel::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/BackgroundVoiceMessageCard.kt b/app/src/main/java/com/nextcloud/talk/ui/BackgroundVoiceMessageCard.kt new file mode 100644 index 0000000..88e75cf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/BackgroundVoiceMessageCard.kt @@ -0,0 +1,195 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.ui + +import android.animation.ValueAnimator +import android.content.Context +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.nextcloud.talk.R +import com.nextcloud.talk.contacts.loadImage +import com.nextcloud.talk.ui.theme.ViewThemeUtils + +@Suppress("LongParameterList") +class BackgroundVoiceMessageCard( + val name: String, + val duration: Int, + private val offset: Float, + private val imageURI: String, + private val conversationImageURI: String, + private var viewThemeUtils: ViewThemeUtils, + private var context: Context +) { + + private val progressState = mutableFloatStateOf(0.0f) + private val animator = ValueAnimator.ofFloat(offset, 1.0f) + + init { + animator.duration = duration.toLong() + animator.addUpdateListener { animation -> + progressState.floatValue = animation.animatedValue as Float + } + + animator.start() + } + + companion object { + private const val ACCOUNT_WEIGHT = .8f + } + + @Suppress("FunctionNaming", "LongMethod") + @Composable + fun GetView(onPlayPaused: (isPaused: Boolean) -> Unit, onClosed: () -> Unit) { + MaterialTheme(colorScheme = viewThemeUtils.getColorScheme(context)) { + Surface( + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier + .padding(16.dp, 0.dp) + ) { + Box( + modifier = Modifier + .background(MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(8.dp)) + .fillMaxWidth(progressState.floatValue) + .height(4.dp) + ) + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .align(Alignment.CenterVertically) + ) { + var isPausedIcon by remember { mutableStateOf(false) } + + IconButton( + onClick = { + isPausedIcon = !isPausedIcon + onPlayPaused(isPausedIcon) + if (isPausedIcon) { + animator.pause() + } else { + animator.resume() + } + } + ) { + Icon( + imageVector = if (isPausedIcon) { + Icons.Filled.PlayArrow + } else { + ImageVector.vectorResource(R.drawable.ic_baseline_pause_voice_message_24) + }, + contentDescription = "contentDescription", + modifier = Modifier + .size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } + Spacer(modifier = Modifier.size(16.dp)) + + Box( + modifier = Modifier + .weight(ACCOUNT_WEIGHT) + .align(Alignment.CenterVertically), + contentAlignment = Alignment.Center + ) { + Row { + Box { + val errorPlaceholderImage: Int = R.drawable.account_circle_96dp + val loadedImage = loadImage(imageURI, context, errorPlaceholderImage) + val conversationImage = loadImage( + conversationImageURI, + context, + errorPlaceholderImage + ) + AsyncImage( + model = conversationImage, + contentDescription = stringResource(R.string.user_avatar), + modifier = Modifier + .size(width = 45.dp, height = 45.dp) + .padding(8.dp) + .offset(10.dp, 10.dp) + ) + + AsyncImage( + model = loadedImage, + contentDescription = stringResource(R.string.user_avatar), + modifier = Modifier + .size(width = 45.dp, height = 45.dp) + .padding(8.dp) + ) + } + + Text( + name, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(8.dp), + color = MaterialTheme.colorScheme.onBackground + ) + } + } + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .align(Alignment.CenterVertically) + ) { + IconButton( + onClick = { + onClosed() + } + ) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "contentDescription", + modifier = Modifier + .size(24.dp) + .padding(2.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt new file mode 100644 index 0000000..b391aca --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt @@ -0,0 +1,1004 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.ui + +import android.content.Context +import android.content.ContextWrapper +import android.util.Log +import android.view.View.TEXT_ALIGNMENT_VIEW_START +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.LinearLayout +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.graphics.ColorUtils +import androidx.emoji2.widget.EmojiTextView +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asFlow +import autodagger.AutoInjector +import coil.compose.AsyncImage +import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.elyeproj.loaderviewlibrary.LoaderTextView +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.MainActivity +import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder.Companion.KEY_MIMETYPE +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.contacts.ContactsViewModel +import com.nextcloud.talk.contacts.load +import com.nextcloud.talk.contacts.loadImage +import com.nextcloud.talk.data.database.mappers.asModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.models.json.opengraph.Reference +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preview.ComposePreviewUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import org.osmdroid.config.Configuration +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.Marker +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import javax.inject.Inject +import kotlin.random.Random + +@Suppress("FunctionNaming", "TooManyFunctions", "LongMethod", "StaticFieldLeak", "LargeClass") +class ComposeChatAdapter( + private var messagesJson: List? = null, + private var messageId: String? = null, + private val utils: ComposePreviewUtils? = null +) { + + interface PreviewAble { + val viewThemeUtils: ViewThemeUtils + val messageUtils: MessageUtils + val contactsViewModel: ContactsViewModel + val chatViewModel: ChatViewModel + val context: Context + val userManager: UserManager + } + + @AutoInjector(NextcloudTalkApplication::class) + inner class ComposeChatAdapterViewModel : + ViewModel(), + PreviewAble { + + @Inject + override lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + override lateinit var messageUtils: MessageUtils + + @Inject + override lateinit var contactsViewModel: ContactsViewModel + + @Inject + override lateinit var chatViewModel: ChatViewModel + + @Inject + override lateinit var context: Context + + @Inject + override lateinit var userManager: UserManager + + init { + sharedApplication?.componentApplication?.inject(this) + } + } + + inner class ComposeChatAdapterPreviewViewModel( + override val viewThemeUtils: ViewThemeUtils, + override val messageUtils: MessageUtils, + override val contactsViewModel: ContactsViewModel, + override val chatViewModel: ChatViewModel, + override val context: Context, + override val userManager: UserManager + ) : ViewModel(), + PreviewAble + + companion object { + val TAG: String = ComposeChatAdapter::class.java.simpleName + private val REGULAR_TEXT_SIZE = 16.sp + private val TIME_TEXT_SIZE = 12.sp + private val AUTHOR_TEXT_SIZE = 12.sp + private const val LONG_1000 = 1000 + private const val SCROLL_DELAY = 20L + private const val QUOTE_SHAPE_OFFSET = 6 + private const val LINE_SPACING = 1.2f + private const val CAPTION_WEIGHT = 0.8f + private const val DEFAULT_WAVE_SIZE = 50 + private const val MAP_ZOOM = 15.0 + private const val INT_8 = 8 + private const val INT_128 = 128 + private const val ANIMATION_DURATION = 2500L + private const val ANIMATED_BLINK = 500 + private const val FLOAT_06 = 0.6f + private const val HALF_OPACITY = 127 + } + + private var incomingShape: RoundedCornerShape = RoundedCornerShape(2.dp, 20.dp, 20.dp, 20.dp) + private var outgoingShape: RoundedCornerShape = RoundedCornerShape(20.dp, 2.dp, 20.dp, 20.dp) + + val viewModel: PreviewAble = + if (utils != null) { + ComposeChatAdapterPreviewViewModel( + utils.viewThemeUtils, + utils.messageUtils, + utils.contactsViewModel, + utils.chatViewModel, + utils.context, + utils.userManager + ) + } else { + ComposeChatAdapterViewModel() + } + + val items = mutableStateListOf() + val currentUser: User = viewModel.userManager.currentUser.blockingGet() + val colorScheme = viewModel.viewThemeUtils.getColorScheme(viewModel.context) + val highEmphasisColorInt = viewModel.context.resources.getColor(R.color.high_emphasis_text, null) + + fun Context.findMainActivityOrNull(): MainActivity? { + var context = this + while (context is ContextWrapper) { + if (context is MainActivity) return context + context = context.baseContext + } + return null + } + + fun addMessages(messages: MutableList, append: Boolean) { + if (messages.isEmpty()) return + + val processedMessages = messages.toMutableList() + if (items.isNotEmpty()) { + if (append) { + processedMessages.add(items.first()) + } else { + processedMessages.add(items.last()) + } + } + + if (append) items.addAll(processedMessages) else items.addAll(0, processedMessages) + } + + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun GetView() { + val listState = rememberLazyListState() + val isBlinkingState = remember { mutableStateOf(true) } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + state = listState, + modifier = Modifier.padding(16.dp) + ) { + stickyHeader { + if (items.size == 0) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + ShimmerGroup() + } + } else { + val timestamp = items[listState.firstVisibleItemIndex].timestamp + val dateString = formatTime(timestamp * LONG_1000) + val color = Color(highEmphasisColorInt) + val backgroundColor = + LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble, null) + Row( + horizontalArrangement = Arrangement.Absolute.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.weight(1f)) + Text( + dateString, + fontSize = AUTHOR_TEXT_SIZE, + color = color, + modifier = Modifier + .padding(8.dp) + .shadow( + 16.dp, + spotColor = colorScheme.primary, + ambientColor = colorScheme.primary + ) + .background(color = Color(backgroundColor), shape = RoundedCornerShape(8.dp)) + .padding(8.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + } + } + } + + items(items) { message -> + message.activeUser = currentUser + when (val type = message.getCalculateMessageType()) { + ChatMessage.MessageType.SYSTEM_MESSAGE -> { + if (!message.shouldFilter()) { + SystemMessage(message) + } + } + + ChatMessage.MessageType.VOICE_MESSAGE -> { + VoiceMessage(message, isBlinkingState) + } + + ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> { + ImageMessage(message, isBlinkingState) + } + + ChatMessage.MessageType.SINGLE_NC_GEOLOCATION_MESSAGE -> { + GeolocationMessage(message, isBlinkingState) + } + + ChatMessage.MessageType.POLL_MESSAGE -> { + PollMessage(message, isBlinkingState) + } + + ChatMessage.MessageType.DECK_CARD -> { + DeckMessage(message, isBlinkingState) + } + + ChatMessage.MessageType.REGULAR_TEXT_MESSAGE -> { + if (message.isLinkPreview()) { + LinkMessage(message, isBlinkingState) + } else { + TextMessage(message, isBlinkingState) + } + } + + else -> { + Log.d(TAG, "Unknown message type: $type") + } + } + } + } + + if (messageId != null && items.size > 0) { + LaunchedEffect(Dispatchers.Main) { + delay(SCROLL_DELAY) + val pos = searchMessages(messageId!!) + if (pos > 0) { + listState.scrollToItem(pos) + } + delay(ANIMATION_DURATION) + isBlinkingState.value = false + } + } + } + + private fun ChatMessage.shouldFilter(): Boolean = + this.isReaction() || + this.isPollVotedMessage() || + this.isEditMessage() || + this.isInfoMessageAboutDeletion() + + private fun ChatMessage.isInfoMessageAboutDeletion(): Boolean = + this.parentMessageId != null && + this.systemMessageType == ChatMessage.SystemMessageType.MESSAGE_DELETED + + private fun ChatMessage.isPollVotedMessage(): Boolean = + this.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED + + private fun ChatMessage.isEditMessage(): Boolean = + this.systemMessageType == ChatMessage.SystemMessageType.MESSAGE_EDITED + + private fun ChatMessage.isReaction(): Boolean = + systemMessageType == ChatMessage.SystemMessageType.REACTION || + systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED || + systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED + + private fun formatTime(timestampMillis: Long): String { + val instant = Instant.ofEpochMilli(timestampMillis) + val dateTime = instant.atZone(ZoneId.systemDefault()).toLocalDate() + val formatter = DateTimeFormatter.ofPattern("MMM dd, yyyy") + return dateTime.format(formatter) + } + + private fun searchMessages(searchId: String): Int { + items.forEachIndexed { index, message -> + if (message.id == searchId) return index + } + return -1 + } + + @Composable + private fun CommonMessageQuote(context: Context, message: ChatMessage) { + val color = colorResource(R.color.high_emphasis_text) + Row( + modifier = Modifier + .drawWithCache { + onDrawWithContent { + drawLine( + color = color, + start = Offset(0f, this.size.height / QUOTE_SHAPE_OFFSET), + end = Offset(0f, this.size.height - (this.size.height / QUOTE_SHAPE_OFFSET)), + strokeWidth = 4f, + cap = StrokeCap.Round + ) + + drawContent() + } + } + .padding(8.dp) + ) { + Column { + Text(message.actorDisplayName!!, fontSize = AUTHOR_TEXT_SIZE) + val imageUri = message.imageUrl + if (imageUri != null) { + val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image + val loadedImage = loadImage(imageUri, context, errorPlaceholderImage) + AsyncImage( + model = loadedImage, + contentDescription = stringResource(R.string.nc_sent_an_image), + modifier = Modifier + .padding(8.dp) + .fillMaxHeight() + ) + } + EnrichedText(message) + } + } + } + + @Composable + private fun CommonMessageBody( + message: ChatMessage, + includePadding: Boolean = true, + playAnimation: Boolean = false, + content: + @Composable + (RowScope.() -> Unit) + ) { + val incoming = message.actorId != currentUser.userId + val color = if (incoming) { + if (message.isDeleted) { + LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble_deleted, null) + } else { + LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble, null) + } + } else { + if (message.isDeleted) { + ColorUtils.setAlphaComponent(colorScheme.surfaceVariant.toArgb(), HALF_OPACITY) + } else { + colorScheme.surfaceVariant.toArgb() + } + } + val shape = if (incoming) incomingShape else outgoingShape + + Row( + modifier = ( + if (message.id == messageId && playAnimation) Modifier.withCustomAnimation(incoming) else Modifier + ) + .fillMaxWidth(1f) + ) { + if (incoming) { + val imageUri = message.actorId?.let { viewModel.contactsViewModel.getImageUri(it, true) } + val errorPlaceholderImage: Int = R.drawable.account_circle_96dp + val loadedImage = loadImage(imageUri, LocalContext.current, errorPlaceholderImage) + AsyncImage( + model = loadedImage, + contentDescription = stringResource(R.string.user_avatar), + modifier = Modifier + .size(48.dp) + .align(Alignment.CenterVertically) + .padding() + .padding(end = 8.dp) + ) + } else { + Spacer(Modifier.weight(1f)) + } + + Surface( + modifier = Modifier + .defaultMinSize(60.dp, 40.dp) + .widthIn(60.dp, 280.dp) + .heightIn(40.dp, 450.dp), + color = Color(color), + shape = shape + ) { + val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp) + val modifier = if (includePadding) Modifier.padding(8.dp, 4.dp, 8.dp, 4.dp) else Modifier + Column(modifier = modifier) { + if (message.parentMessageId != null && !message.isDeleted && messagesJson != null) { + messagesJson!! + .find { it.parentMessage?.id == message.parentMessageId } + ?.parentMessage!!.asModel().let { CommonMessageQuote(LocalContext.current, it) } + } + + if (incoming) { + Text(message.actorDisplayName.toString(), fontSize = AUTHOR_TEXT_SIZE) + } + + Row { + content() + Spacer(modifier = Modifier.size(8.dp)) + Text( + timeString, + fontSize = TIME_TEXT_SIZE, + textAlign = TextAlign.End, + modifier = Modifier.align(Alignment.CenterVertically) + ) + if (message.readStatus == ReadStatus.NONE) { + val read = painterResource(R.drawable.ic_check_all) + Icon( + read, + "", + modifier = Modifier + .padding(start = 2.dp) + .size(12.dp) + .align(Alignment.CenterVertically) + ) + } + } + } + } + } + } + + @Composable + private fun Modifier.withCustomAnimation(incoming: Boolean): Modifier { + val infiniteTransition = rememberInfiniteTransition() + val borderColor by infiniteTransition.animateColor( + initialValue = colorScheme.primary, + targetValue = colorScheme.background, + animationSpec = infiniteRepeatable( + animation = tween(ANIMATED_BLINK, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ) + ) + + return this.border( + width = 4.dp, + color = borderColor, + shape = if (incoming) incomingShape else outgoingShape + ) + } + + @Composable + private fun ShimmerGroup() { + Shimmer() + Shimmer(true) + Shimmer() + Shimmer(true) + Shimmer(true) + Shimmer() + Shimmer(true) + } + + @Composable + private fun Shimmer(outgoing: Boolean = false) { + Row(modifier = Modifier.padding(top = 16.dp)) { + if (!outgoing) { + ShimmerImage(this) + } + + val v1 by remember { mutableIntStateOf((INT_8..INT_128).random()) } + val v2 by remember { mutableIntStateOf((INT_8..INT_128).random()) } + val v3 by remember { mutableIntStateOf((INT_8..INT_128).random()) } + + Column { + ShimmerText(this, v1, outgoing) + ShimmerText(this, v2, outgoing) + ShimmerText(this, v3, outgoing) + } + } + } + + @Composable + private fun ShimmerImage(rowScope: RowScope) { + rowScope.apply { + AndroidView( + factory = { ctx -> + LoaderImageView(ctx).apply { + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + val color = resources.getColor(R.color.nc_shimmer_default_color, null) + setBackgroundColor(color) + } + }, + modifier = Modifier + .clip(CircleShape) + .size(40.dp) + .align(Alignment.Top) + ) + } + } + + @Composable + private fun ShimmerText(columnScope: ColumnScope, margin: Int, outgoing: Boolean = false) { + columnScope.apply { + AndroidView( + factory = { ctx -> + LoaderTextView(ctx).apply { + layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + val color = if (outgoing) { + colorScheme.primary.toArgb() + } else { + resources.getColor(R.color.nc_shimmer_default_color, null) + } + + setBackgroundColor(color) + } + }, + modifier = Modifier.padding( + top = 6.dp, + end = if (!outgoing) margin.dp else 8.dp, + start = if (outgoing) margin.dp else 8.dp + ) + ) + } + } + + @Composable + private fun EnrichedText(message: ChatMessage) { + AndroidView(factory = { ctx -> + val incoming = message.actorId != currentUser.userId + var processedMessageText = viewModel.messageUtils.enrichChatMessageText( + ctx, + message, + incoming, + viewModel.viewThemeUtils + ) + + processedMessageText = viewModel.messageUtils.processMessageParameters( + ctx, + viewModel.viewThemeUtils, + processedMessageText!!, + message, + null + ) + + EmojiTextView(ctx).apply { + layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + setLineSpacing(0F, LINE_SPACING) + textAlignment = TEXT_ALIGNMENT_VIEW_START + text = processedMessageText + setPadding(0, INT_8, 0, 0) + } + }, modifier = Modifier) + } + + @Composable + private fun TextMessage(message: ChatMessage, state: MutableState) { + CommonMessageBody(message, playAnimation = state.value) { + EnrichedText(message) + } + } + + @Composable + fun SystemMessage(message: ChatMessage) { + val similarMessages = sharedApplication!!.resources.getQuantityString( + R.plurals.see_similar_system_messages, + message.expandableChildrenAmount, + message.expandableChildrenAmount + ) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp) + Row(horizontalArrangement = Arrangement.Absolute.Center, verticalAlignment = Alignment.CenterVertically) { + Spacer(modifier = Modifier.weight(1f)) + Text( + message.text, + fontSize = AUTHOR_TEXT_SIZE, + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(FLOAT_06) + ) + Text( + timeString, + fontSize = TIME_TEXT_SIZE, + textAlign = TextAlign.End, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.weight(1f)) + } + + if (message.expandableChildrenAmount > 0) { + TextButtonNoStyling(similarMessages) { + // NOTE: Read only for now + } + } + } + } + + @Composable + private fun TextButtonNoStyling(text: String, onClick: () -> Unit) { + TextButton(onClick = onClick) { + Text( + text, + fontSize = AUTHOR_TEXT_SIZE, + color = Color(highEmphasisColorInt) + ) + } + } + + @Composable + private fun ImageMessage(message: ChatMessage, state: MutableState) { + val hasCaption = (message.message != "{file}") + val incoming = message.actorId != currentUser.userId + val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp) + CommonMessageBody(message, includePadding = false, playAnimation = state.value) { + Column { + message.activeUser = currentUser + val imageUri = message.imageUrl + val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE] + val drawableResourceId = getDrawableResourceIdForMimeType(mimetype) + val loadedImage = load(imageUri, LocalContext.current, drawableResourceId) + + AsyncImage( + model = loadedImage, + contentDescription = stringResource(R.string.nc_sent_an_image), + modifier = Modifier + .fillMaxWidth(), + contentScale = ContentScale.FillWidth + ) + + if (hasCaption) { + Text( + message.text, + fontSize = 12.sp, + modifier = Modifier + .widthIn(20.dp, 140.dp) + .padding(8.dp) + ) + } + } + } + + if (!hasCaption) { + Row(modifier = Modifier.padding(top = 8.dp), verticalAlignment = Alignment.CenterVertically) { + if (!incoming) { + Spacer(Modifier.weight(1f)) + } else { + Spacer(Modifier.size(width = 56.dp, 0.dp)) // To account for avatar size + } + Text(message.text, fontSize = 12.sp) + Text( + timeString, + fontSize = TIME_TEXT_SIZE, + textAlign = TextAlign.End, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding() + .padding(start = 4.dp) + ) + if (message.readStatus == ReadStatus.NONE) { + val read = painterResource(R.drawable.ic_check_all) + Icon( + read, + "", + modifier = Modifier + .padding(start = 2.dp) + .size(12.dp) + .align(Alignment.CenterVertically) + ) + } + } + } + } + + @Composable + private fun VoiceMessage(message: ChatMessage, state: MutableState) { + CommonMessageBody(message, playAnimation = state.value) { + Icon( + Icons.Filled.PlayArrow, + "play", + modifier = Modifier + .size(24.dp) + .align(Alignment.CenterVertically) + ) + + AndroidView( + factory = { ctx -> + WaveformSeekBar(ctx).apply { + setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now + setColors( + colorScheme.inversePrimary.toArgb(), + colorScheme.onPrimaryContainer.toArgb() + ) + } + }, + modifier = Modifier + .align(Alignment.CenterVertically) + .width(180.dp) + .height(80.dp) + ) + } + } + + @Composable + private fun GeolocationMessage(message: ChatMessage, state: MutableState) { + CommonMessageBody(message, playAnimation = state.value) { + Column { + if (message.messageParameters != null && message.messageParameters!!.isNotEmpty()) { + for (key in message.messageParameters!!.keys) { + val individualHashMap: Map = message.messageParameters!![key]!! + if (individualHashMap["type"] == "geo-location") { + val lat = individualHashMap["latitude"] + val lng = individualHashMap["longitude"] + + if (lat != null && lng != null) { + val latitude = lat.toDouble() + val longitude = lng.toDouble() + OpenStreetMap(latitude, longitude) + } + } + } + } + } + } + } + + @Composable + private fun OpenStreetMap(latitude: Double, longitude: Double) { + AndroidView( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(), + factory = { context -> + Configuration.getInstance().userAgentValue = context.packageName + MapView(context).apply { + setTileSource(TileSourceFactory.MAPNIK) + setMultiTouchControls(true) + + val geoPoint = GeoPoint(latitude, longitude) + controller.setCenter(geoPoint) + controller.setZoom(MAP_ZOOM) + + val marker = Marker(this) + marker.position = geoPoint + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + marker.title = "Location" + overlays.add(marker) + + invalidate() + } + }, + update = { mapView -> + val geoPoint = GeoPoint(latitude, longitude) + mapView.controller.setCenter(geoPoint) + + val marker = mapView.overlays.find { it is Marker } as? Marker + marker?.position = geoPoint + mapView.invalidate() + } + ) + } + + @Composable + private fun LinkMessage(message: ChatMessage, state: MutableState) { + val color = colorResource(R.color.high_emphasis_text) + viewModel.chatViewModel.getOpenGraph( + currentUser.getCredentials(), + currentUser.baseUrl!!, + message.extractedUrlToPreview!! + ) + CommonMessageBody(message, playAnimation = state.value) { + Row( + modifier = Modifier + .drawWithCache { + onDrawWithContent { + drawLine( + color = color, + start = Offset.Zero, + end = Offset(0f, this.size.height), + strokeWidth = 4f, + cap = StrokeCap.Round + ) + + drawContent() + } + } + .padding(8.dp) + .padding(4.dp) + ) { + Column { + val graphObject = viewModel.chatViewModel.getOpenGraph.asFlow().collectAsState( + Reference( + // Dummy class + ) + ).value.openGraphObject + graphObject?.let { + Text(it.name, fontSize = REGULAR_TEXT_SIZE, fontWeight = FontWeight.Bold) + it.description?.let { Text(it, fontSize = AUTHOR_TEXT_SIZE) } + it.link?.let { Text(it, fontSize = TIME_TEXT_SIZE) } + it.thumb?.let { + val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image + val loadedImage = loadImage(it, LocalContext.current, errorPlaceholderImage) + AsyncImage( + model = loadedImage, + contentDescription = stringResource(R.string.nc_sent_an_image), + modifier = Modifier + .height(120.dp) + ) + } + } + } + } + } + } + + @Composable + private fun PollMessage(message: ChatMessage, state: MutableState) { + CommonMessageBody(message, playAnimation = state.value) { + Column { + if (message.messageParameters != null && message.messageParameters!!.size > 0) { + for (key in message.messageParameters!!.keys) { + val individualHashMap: Map = message.messageParameters!![key]!! + if (individualHashMap["type"] == "talk-poll") { + // val pollId = individualHashMap["id"] + val pollName = individualHashMap["name"].toString() + Row(modifier = Modifier.padding(start = 8.dp)) { + Icon(painterResource(R.drawable.ic_baseline_bar_chart_24), "") + Text(pollName, fontSize = AUTHOR_TEXT_SIZE, fontWeight = FontWeight.Bold) + } + + TextButtonNoStyling(stringResource(R.string.message_poll_tap_to_open)) { + // NOTE: read only for now + } + } + } + } + } + } + } + + @Composable + private fun DeckMessage(message: ChatMessage, state: MutableState) { + CommonMessageBody(message, playAnimation = state.value) { + Column { + if (message.messageParameters != null && message.messageParameters!!.size > 0) { + for (key in message.messageParameters!!.keys) { + val individualHashMap: Map = message.messageParameters!![key]!! + if (individualHashMap["type"] == "deck-card") { + val cardName = individualHashMap["name"] + val stackName = individualHashMap["stackname"] + val boardName = individualHashMap["boardname"] + // val cardLink = individualHashMap["link"] + + if (cardName?.isNotEmpty() == true) { + val cardDescription = String.format( + LocalContext.current.resources.getString(R.string.deck_card_description), + stackName, + boardName + ) + Row(modifier = Modifier.padding(start = 8.dp)) { + Icon(painterResource(R.drawable.deck), "") + Text(cardName, fontSize = AUTHOR_TEXT_SIZE, fontWeight = FontWeight.Bold) + } + Text(cardDescription, fontSize = AUTHOR_TEXT_SIZE) + } + } + } + } + } + } + } +} + +@Preview(showBackground = true, widthDp = 380, heightDp = 800) +@Composable +fun AllMessageTypesPreview() { + val previewUtils = ComposePreviewUtils.getInstance(LocalContext.current) + val adapter = remember { ComposeChatAdapter(messagesJson = null, messageId = null, previewUtils) } + + val sampleMessages = remember { + listOf( + // Text Messages + ChatMessage().apply { + jsonMessageId = 1 + actorId = "user1" + message = "I love Nextcloud" + timestamp = System.currentTimeMillis() + actorDisplayName = "User1" + messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name + }, + ChatMessage().apply { + jsonMessageId = 2 + actorId = "user1_id" + message = "I love Nextcloud" + timestamp = System.currentTimeMillis() + actorDisplayName = "User2" + messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name + } + ) + } + + LaunchedEffect(sampleMessages) { + // Use LaunchedEffect or similar to update state once + if (adapter.items.isEmpty()) { + // Prevent adding multiple times on recomposition + adapter.addMessages(sampleMessages.toMutableList(), append = false) // Add messages + } + } + + MaterialTheme(colorScheme = adapter.colorScheme) { + // Use the (potentially faked) color scheme + Box(modifier = Modifier.fillMaxSize()) { + // Provide a container + adapter.GetView() // Call the main Composable + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt b/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt new file mode 100644 index 0000000..266cd73 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/MessageInput.kt @@ -0,0 +1,81 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.Chronometer +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.SeekBar +import android.widget.TextView +import androidx.emoji2.widget.EmojiEditText +import com.google.android.material.button.MaterialButton +import com.nextcloud.talk.R +import com.stfalcon.chatkit.messages.MessageInput + +class MessageInput : MessageInput { + lateinit var audioRecordDuration: Chronometer + lateinit var recordAudioButton: ImageButton + lateinit var submitThreadButton: ImageButton + lateinit var slideToCancelDescription: TextView + lateinit var microphoneEnabledInfo: ImageView + lateinit var microphoneEnabledInfoBackground: ImageView + lateinit var smileyButton: ImageButton + lateinit var deleteVoiceRecording: ImageView + lateinit var sendVoiceRecording: ImageView + lateinit var micInputCloud: MicInputCloud + lateinit var playPauseBtn: MaterialButton + lateinit var editMessageButton: ImageButton + lateinit var seekBar: SeekBar + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + init() + } + + private fun init() { + audioRecordDuration = findViewById(R.id.audioRecordDuration) + recordAudioButton = findViewById(R.id.recordAudioButton) + submitThreadButton = findViewById(R.id.submitThreadButton) + slideToCancelDescription = findViewById(R.id.slideToCancelDescription) + microphoneEnabledInfo = findViewById(R.id.microphoneEnabledInfo) + microphoneEnabledInfoBackground = findViewById(R.id.microphoneEnabledInfoBackground) + smileyButton = findViewById(R.id.smileyButton) + deleteVoiceRecording = findViewById(R.id.deleteVoiceRecording) + sendVoiceRecording = findViewById(R.id.sendVoiceRecording) + micInputCloud = findViewById(R.id.micInputCloud) + playPauseBtn = findViewById(R.id.playPauseBtn) + seekBar = findViewById(R.id.seekbar) + editMessageButton = findViewById(R.id.editMessageButton) + } + + var messageInput: EmojiEditText + get() = super.messageInput + set(messageInput) { + super.messageInput = messageInput + } + + var attachmentButton: ImageButton + get() = super.attachmentButton + set(attachmentButton) { + super.attachmentButton = attachmentButton + } + + var messageSendButton: ImageButton + get() = super.messageSendButton + set(messageSendButton) { + super.messageSendButton = messageSendButton + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/MicInputCloud.kt b/app/src/main/java/com/nextcloud/talk/ui/MicInputCloud.kt new file mode 100644 index 0000000..276bd67 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/MicInputCloud.kt @@ -0,0 +1,382 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui + +import android.animation.ValueAnimator +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Paint.ANTI_ALIAS_FLAG +import android.graphics.Path +import android.graphics.Rect +import android.graphics.drawable.VectorDrawable +import android.util.AttributeSet +import android.view.View +import android.view.animation.LinearInterpolator +import androidx.annotation.ColorInt +import com.nextcloud.talk.R +import kotlin.math.roundToInt + +class MicInputCloud(context: Context, attrs: AttributeSet) : View(context, attrs) { + /** + * State Descriptions: + * - PAUSED_STATE: Animation speed is set to zero. + * - PLAY_STATE: Animation speed is set to default, but can be overridden. + */ + enum class ViewState { + /** + * Animation speed is set to zero. + */ + PAUSED_STATE, + + /** + * Animation speed is set to default, but can be overridden. + */ + PLAY_STATE + } + + @ColorInt + private var primaryColor: Int = Color.WHITE + + private var pauseIcon: VectorDrawable? = null + + private var playIcon: VectorDrawable? = null + + init { + context.theme.obtainStyledAttributes( + attrs, + R.styleable.MicInputCloud, + 0, + 0 + ).apply { + + try { + pauseIcon = getDrawable(R.styleable.MicInputCloud_pauseIcon) as VectorDrawable + playIcon = getDrawable(R.styleable.MicInputCloud_playIcon) as VectorDrawable + } finally { + recycle() + } + } + } + + private var state: ViewState = ViewState.PLAY_STATE + private var ovalOneAnimator: ValueAnimator? = null + private var ovalTwoAnimator: ValueAnimator? = null + private var ovalThreeAnimator: ValueAnimator? = null + private var r1 = OVAL_ONE_DEFAULT_ROTATION + private var r2 = OVAL_TWO_DEFAULT_ROTATION + private var r3 = OVAL_THREE_DEFAULT_ROTATION + private var o1h = OVAL_ONE_DEFAULT_HEIGHT + private var o1w = OVAL_ONE_DEFAULT_WIDTH + private var o2h = OVAL_TWO_DEFAULT_HEIGHT + private var o2w = OVAL_TWO_DEFAULT_WIDTH + private var o3h = OVAL_THREE_DEFAULT_HEIGHT + private var o3w = OVAL_THREE_DEFAULT_WIDTH + private var rotationSpeedMultiplier: Float = DEFAULT_ROTATION_SPEED_MULTIPLIER + private var radius: Float = DEFAULT_RADIUS + private var centerX: Float = 0f + private var centerY: Float = 0f + + private val bottomCirclePaint = Paint(ANTI_ALIAS_FLAG).apply { + color = primaryColor + style = Paint.Style.FILL + alpha = DEFAULT_OPACITY + } + + private val topCircleBounds = Rect(0, 0, 0, 0) + private val iconBounds = topCircleBounds + + override fun onVisibilityChanged(changedView: View, visibility: Int) { + super.onVisibilityChanged(changedView, visibility) + if (visibility == VISIBLE) { + createAnimators() + } else { + state = ViewState.PLAY_STATE + destroyAnimators() + } + } + + private fun createAnimators() { + ovalOneAnimator = ValueAnimator.ofInt( + o1h, + OVAL_ONE_DEFAULT_HEIGHT + ANIMATION_CAP, + o1h + ).apply { + duration = OVAL_ONE_ANIMATION_LENGTH + interpolator = LinearInterpolator() + repeatCount = ValueAnimator.INFINITE + addUpdateListener { valueAnimator -> + o1h = valueAnimator.animatedValue as Int + } + } + + ovalTwoAnimator = ValueAnimator.ofInt( + o2h, + OVAL_TWO_DEFAULT_HEIGHT + ANIMATION_CAP, + o2h + ).apply { + duration = OVAL_TWO_ANIMATION_LENGTH + interpolator = LinearInterpolator() + repeatCount = ValueAnimator.INFINITE + addUpdateListener { valueAnimator -> + o2h = valueAnimator.animatedValue as Int + } + } + + ovalThreeAnimator = ValueAnimator.ofInt( + o3h, + OVAL_THREE_DEFAULT_HEIGHT + ANIMATION_CAP, + o3h + ).apply { + duration = OVAL_THREE_ANIMATION_LENGTH + interpolator = LinearInterpolator() + repeatCount = ValueAnimator.INFINITE + addUpdateListener { valueAnimator -> + o3h = valueAnimator.animatedValue as Int + invalidate() // needed to animate the other listeners as well + } + } + } + + private fun destroyAnimators() { + ovalOneAnimator?.cancel() + ovalOneAnimator?.removeAllUpdateListeners() + ovalTwoAnimator?.cancel() + ovalTwoAnimator?.removeAllUpdateListeners() + ovalThreeAnimator?.cancel() + ovalThreeAnimator?.removeAllUpdateListeners() + } + + private val circlePath: Path = Path() + private val ovalOnePath: Path = Path() + private val ovalTwoPath: Path = Path() + private val ovalThreePath: Path = Path() + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + circlePath.apply { + addCircle(centerX, centerY, DEFAULT_RADIUS, Path.Direction.CCW) + } + ovalOnePath.apply { + addOval( + centerX - (radius + o1w), + centerY - o1h, + centerX + (radius + o1w), + centerY + o1h, + Path.Direction.CCW + ) + op(this, circlePath, Path.Op.DIFFERENCE) + } + ovalTwoPath.apply { + addOval( + centerX - (radius + o2w), + centerY - o2h, + centerX + (radius + o2w), + centerY + o2h, + Path.Direction.CCW + ) + op(this, circlePath, Path.Op.DIFFERENCE) + } + ovalThreePath.apply { + addOval( + centerX - (radius + o3w), + centerY - o3h, + centerX + (radius + o3w), + centerY + o3h, + Path.Direction.CCW + ) + op(this, circlePath, Path.Op.DIFFERENCE) + } + drawMicInputCloud(canvas) + if (state == ViewState.PLAY_STATE) { + r1 += OVAL_ONE_ANIMATION_SPEED * rotationSpeedMultiplier + r2 -= OVAL_TWO_ANIMATION_SPEED * rotationSpeedMultiplier + r3 += OVAL_THREE_ANIMATION_SPEED * rotationSpeedMultiplier + invalidate() + } + } + + private fun drawMicInputCloud(canvas: Canvas?) { + canvas?.apply { + save() + rotate(r1, centerX, centerY) + drawPath(ovalOnePath, bottomCirclePaint) + restore() + save() + rotate(r2, centerX, centerY) + drawPath(ovalTwoPath, bottomCirclePaint) + restore() + save() + rotate(r3, centerX, centerY) + drawPath(ovalThreePath, bottomCirclePaint) + restore() + circlePath.reset() + ovalOnePath.reset() + ovalTwoPath.reset() + ovalThreePath.reset() + if (state == ViewState.PLAY_STATE) { + pauseIcon?.apply { + bounds = topCircleBounds + setTint(primaryColor) + draw(canvas) + } + } else { + playIcon?.apply { + bounds = topCircleBounds + setTint(primaryColor) + draw(canvas) + } + } + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val desiredWidth = DEFAULT_SIZE.dp + val desiredHeight = DEFAULT_SIZE.dp + + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + val width: Int = when (widthMode) { + MeasureSpec.EXACTLY -> { + widthSize + } + + MeasureSpec.AT_MOST -> { + desiredWidth.coerceAtMost(widthSize) + } + + else -> { + desiredWidth + } + } + + val height: Int = when (heightMode) { + MeasureSpec.EXACTLY -> { + heightSize + } + + MeasureSpec.AT_MOST -> { + desiredHeight.coerceAtMost(heightSize) + } + + else -> { + desiredHeight + } + } + + centerX = (width / 2).toFloat() + centerY = (height / 2).toFloat() + topCircleBounds.apply { + left = (centerX - DEFAULT_RADIUS).toInt() + top = (centerY - DEFAULT_RADIUS).toInt() + right = (centerX + DEFAULT_RADIUS).toInt() + bottom = (centerY + DEFAULT_RADIUS).toInt() + } + + /** + * Drawables are drawn the same way as the canvas is drawn, as both originate from the top-left corner. + * Because of this, the icon's width = (right - left) and height = (bottom - top). + */ + iconBounds.apply { + left = (centerX - DEFAULT_RADIUS + ICON_SIZE.dp).toInt() + top = (centerY - DEFAULT_RADIUS + ICON_SIZE.dp).toInt() + right = (centerX + DEFAULT_RADIUS - ICON_SIZE.dp).toInt() + bottom = (centerY + DEFAULT_RADIUS - ICON_SIZE.dp).toInt() + } + + setMeasuredDimension(width, height) + } + + override fun performClick(): Boolean { + state = if (state == ViewState.PAUSED_STATE) { + ovalOneAnimator?.resume() + ovalTwoAnimator?.resume() + ovalThreeAnimator?.resume() + ViewState.PLAY_STATE + } else { + ovalOneAnimator?.pause() + ovalTwoAnimator?.pause() + ovalThreeAnimator?.pause() + ViewState.PAUSED_STATE + } + invalidate() + return super.performClick() + } + + /** + * Sets the color of the cloud to the parameter, opacity is still set to 50%. + */ + fun setColor(primary: Int) { + primaryColor = primary + bottomCirclePaint.apply { + color = primary + style = Paint.Style.FILL + alpha = DEFAULT_OPACITY + } + invalidate() + } + + /** + * Sets state of the component to the parameter, must be of type MicInputCloud.ViewState. + */ + fun setState(s: ViewState) { + state = s + invalidate() + } + + /** + * Sets the rotation speed and radius to the parameters, defaults are left unchanged. + */ + fun setRotationSpeed(speed: Float, r: Float) { + rotationSpeedMultiplier = speed + radius = r + invalidate() + } + + /** + * Starts the growing and shrinking animation + */ + fun startAnimators() { + ovalOneAnimator?.start() + ovalTwoAnimator?.start() + ovalThreeAnimator?.start() + } + + companion object { + val TAG: String? = MicInputCloud::class.simpleName + const val DEFAULT_RADIUS: Float = 70f + const val EXTENDED_RADIUS: Float = 75f + const val MAXIMUM_RADIUS: Float = 80f + const val ICON_SIZE: Int = 9 // Converted to dp this equals about 24dp + private const val DEFAULT_SIZE: Int = 110 + private const val DEFAULT_OPACITY: Int = 108 + private const val DEFAULT_ROTATION_SPEED_MULTIPLIER: Float = 0.5f + private const val OVAL_ONE_DEFAULT_ROTATION: Float = 105f + private const val OVAL_ONE_DEFAULT_HEIGHT: Int = 85 + private const val OVAL_ONE_DEFAULT_WIDTH: Int = 30 + private const val OVAL_ONE_ANIMATION_LENGTH: Long = 2000 + private const val OVAL_ONE_ANIMATION_SPEED: Float = 2.3f + private const val OVAL_TWO_DEFAULT_ROTATION: Float = 138f + private const val OVAL_TWO_DEFAULT_HEIGHT: Int = 70 + private const val OVAL_TWO_DEFAULT_WIDTH: Int = 25 + private const val OVAL_TWO_ANIMATION_LENGTH: Long = 1000 + private const val OVAL_TWO_ANIMATION_SPEED: Float = 1.75f + private const val OVAL_THREE_DEFAULT_ROTATION: Float = 63f + private const val OVAL_THREE_DEFAULT_HEIGHT: Int = 80 + private const val OVAL_THREE_DEFAULT_WIDTH: Int = 40 + private const val OVAL_THREE_ANIMATION_LENGTH: Long = 1500 + private const val OVAL_THREE_ANIMATION_SPEED: Float = 1f + private const val ANIMATION_CAP: Int = 15 + private val Int.dp: Int + get() = (this * Resources.getSystem().displayMetrics.density).roundToInt() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt b/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt new file mode 100644 index 0000000..348138e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/PlaybackSpeedControl.kt @@ -0,0 +1,71 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.ui + +import android.content.Context +import android.util.AttributeSet +import autodagger.AutoInjector +import com.google.android.material.button.MaterialButton +import java.util.Locale +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import javax.inject.Inject + +internal const val SPEED_FACTOR_SLOW = 0.8f +internal const val SPEED_FACTOR_NORMAL = 1.0f +internal const val SPEED_FACTOR_FASTER = 1.5f +internal const val SPEED_FACTOR_FASTEST = 2.0f + +@AutoInjector(NextcloudTalkApplication::class) +class PlaybackSpeedControl @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialButton(context, attrs, defStyleAttr) { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private var currentSpeed = PlaybackSpeed.NORMAL + + init { + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + text = currentSpeed.label + viewThemeUtils.material.colorMaterialButtonText(this) + } + + fun setSpeed(newSpeed: PlaybackSpeed) { + currentSpeed = newSpeed + text = currentSpeed.label + } + + fun getSpeed(): PlaybackSpeed = currentSpeed +} + +enum class PlaybackSpeed(val value: Float) { + SLOW(SPEED_FACTOR_SLOW), + NORMAL(SPEED_FACTOR_NORMAL), + FASTER(SPEED_FACTOR_FASTER), + FASTEST(SPEED_FACTOR_FASTEST); + + // no fixed, literal labels, since we want to obey numeric interpunctuation for different locales + val label: String = String.format(Locale.getDefault(), "%01.1fx", value) + + fun next(): PlaybackSpeed = entries[(ordinal + 1) % entries.size] + + companion object { + fun byName(name: String): PlaybackSpeed { + for (speed in entries) { + if (speed.name.equals(name, ignoreCase = true)) { + return speed + } + } + return NORMAL + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/StatusDrawable.java b/app/src/main/java/com/nextcloud/talk/ui/StatusDrawable.java new file mode 100644 index 0000000..4c34459 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/StatusDrawable.java @@ -0,0 +1,144 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; + +import com.nextcloud.talk.R; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.core.content.res.ResourcesCompat; + +/** + * A Drawable object that draws a status + */ +public class StatusDrawable extends Drawable { + private String text; + private StatusDrawableType icon = StatusDrawableType.UNDEFINED; + private Paint textPaint; + private int backgroundColor; + private final float radius; + private Context context; + + public void colorStatusDrawable(@ColorInt int color) { + backgroundColor = color; + invalidateSelf(); + } + + public StatusDrawable(String status, String statusIcon, float statusSize, int backgroundColor, + Context context) { + radius = statusSize; + this.backgroundColor = backgroundColor; + + + if ("dnd".equals(status)) { + icon = StatusDrawableType.DND; + this.context = context; + } else if (TextUtils.isEmpty(statusIcon) && status != null) { + switch (status) { + case "online" -> { + icon = StatusDrawableType.ONLINE; + this.context = context; + } + + case "busy" -> { + icon = StatusDrawableType.BUSY; + this.context = context; + } + case "away" -> { + icon = StatusDrawableType.AWAY; + this.context = context; + } + + default -> { + } + // do not show + } + } else { + text = statusIcon; + + textPaint = new Paint(); + textPaint.setTextSize(statusSize); + textPaint.setAntiAlias(true); + textPaint.setTextAlign(Paint.Align.CENTER); + } + } + + /** + * Draw in its bounds (set via setBounds) respecting optional effects such as alpha (set via setAlpha) and color + * filter (set via setColorFilter) a circular background with a user's first character. + * + * @param canvas The canvas to draw into + */ + @Override + public void draw(@NonNull Canvas canvas) { + if (text != null) { + textPaint.setTextSize(1.6f * radius); + canvas.drawText(text, radius, radius - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint); + } + + if (icon != StatusDrawableType.UNDEFINED) { + + Paint backgroundPaint = new Paint(); + backgroundPaint.setStyle(Paint.Style.FILL); + backgroundPaint.setAntiAlias(true); + backgroundPaint.setColor(backgroundColor); + + canvas.drawCircle(radius, radius, radius, backgroundPaint); + + Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), icon.drawableId, null); + + if (drawable != null) { + drawable.setBounds(0, + 0, + (int) (2 * radius), + (int) (2 * radius)); + drawable.draw(canvas); + } + } + } + + @Override + public void setAlpha(int alpha) { + textPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + textPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + private enum StatusDrawableType { + DND(R.drawable.ic_user_status_dnd), + ONLINE(R.drawable.online_status), + BUSY(R.drawable.ic_user_status_busy), + AWAY(R.drawable.ic_user_status_away), + UNDEFINED(-1); + + @DrawableRes + private final int drawableId; + + StatusDrawableType(int drawableId) { + this.drawableId = drawableId; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt b/app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt new file mode 100644 index 0000000..06e5661 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/WaveformSeekBar.kt @@ -0,0 +1,125 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui + +import android.content.Context +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import androidx.annotation.ColorInt +import androidx.appcompat.widget.AppCompatSeekBar +import androidx.core.graphics.toColorInt +import com.nextcloud.talk.utils.AudioUtils +import kotlin.math.roundToInt + +class WaveformSeekBar : AppCompatSeekBar { + + @ColorInt + private var primary: Int = "#679ff5".toColorInt() + + @ColorInt + private var secondary: Int = "#a6c6f7".toColorInt() + private var rawData: FloatArray = floatArrayOf() + private var waveData: FloatArray = floatArrayOf() + private var savedMeasure: Int = 0 + private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + init() + } + + fun setColors(@ColorInt p: Int, @ColorInt s: Int) { + primary = p + secondary = s + invalidate() + } + + /** + * Sets the wave data of the seekbar. Shrinks the data to a calculated number of bars based off the width of the + * seekBar. The greater the width, the more bars displayed. + * + * Note: bar gap = (usableWidth - waveData.size * DEFAULT_BAR_WIDTH) / (waveData.size - 1).toFloat() + * therefore, the gap is determined by the width of the seekBar by extension. + */ + fun setWaveData(data: FloatArray) { + rawData = data + } + + private fun init() { + paint.apply { + strokeCap = Paint.Cap.ROUND + strokeWidth = DEFAULT_BAR_WIDTH.dp.toFloat() + color = Color.RED + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val usableWidth = measuredWidth - paddingLeft - paddingRight + if (usableWidth > MINIMUM_WIDTH && rawData.isNotEmpty() && usableWidth != savedMeasure) { + savedMeasure = usableWidth + val numBars = if (usableWidth > VALUE_100) (usableWidth / WIDTH_DIVISOR) else usableWidth / 2f + waveData = AudioUtils.shrinkFloatArray(rawData, numBars.roundToInt()) + invalidate() + } + } + + override fun onDraw(canvas: Canvas) { + if (waveData.isEmpty() || waveData[0].toString() == "NaN") { + super.onDraw(canvas) + } else { + if (progressDrawable != null) { + super.setProgressDrawable(null) + } + + drawWaveformSeekbar(canvas) + super.onDraw(canvas) + } + } + + private fun drawWaveformSeekbar(canvas: Canvas?) { + val usableHeight = height - paddingTop - paddingBottom + val usableWidth = width - paddingLeft - paddingRight + val midpoint = usableHeight / 2f + val maxHeight: Float = usableHeight / MAX_HEIGHT_DIVISOR + val barGap: Float = (usableWidth - waveData.size * DEFAULT_BAR_WIDTH) / (waveData.size - 1).toFloat() + + canvas?.apply { + save() + translate(paddingLeft.toFloat(), paddingTop.toFloat()) + for (i in waveData.indices) { + val x: Float = i * (DEFAULT_BAR_WIDTH + barGap) + DEFAULT_BAR_WIDTH / 2f + val y: Float = waveData[i] * maxHeight + val progress = (x / usableWidth) + paint.color = if (progress * max < getProgress()) primary else secondary + canvas.drawLine(x, midpoint - y, x, midpoint + y, paint) + } + + restore() + } + } + + companion object { + private const val DEFAULT_BAR_WIDTH: Int = 2 + private const val MAX_HEIGHT_DIVISOR: Float = 4.0f + private const val WIDTH_DIVISOR = 20f + private const val VALUE_100 = 100 + private const val MINIMUM_WIDTH = 50 + private val Int.dp: Int + get() = (this * Resources.getSystem().displayMetrics.density).roundToInt() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt b/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt new file mode 100644 index 0000000..814692f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/bottom/sheet/ProfileBottomSheet.kt @@ -0,0 +1,184 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.bottom.sheet + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.core.net.toUri +import com.afollestad.materialdialogs.LayoutMode +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.bottomsheets.BottomSheet +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.bottomsheet.items.BasicListItemWithImage +import com.nextcloud.talk.bottomsheet.items.listItemsWithImage +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.hovercard.HoverCardAction +import com.nextcloud.talk.models.json.hovercard.HoverCardOverall +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet.AllowedAppIds.EMAIL +import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet.AllowedAppIds.PROFILE +import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet.AllowedAppIds.SPREED +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers + +private const val TAG = "ProfileBottomSheet" + +class ProfileBottomSheet(val ncApi: NcApi, val userModel: User, val viewThemeUtils: ViewThemeUtils) { + + private val allowedAppIds = listOf(SPREED.stringValue, PROFILE.stringValue, EMAIL.stringValue) + + fun showFor(message: ChatMessage, context: Context) { + if (message.actorType == Participant.ActorType.FEDERATED.toString()) { + Log.d(TAG, "no actions for federated users are shown") + return + } + + ncApi.hoverCard( + ApiUtils.getCredentials(userModel.username, userModel.token), + ApiUtils.getUrlForHoverCard(userModel.baseUrl!!, message.actorId!!) + ).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(hoverCardOverall: HoverCardOverall) { + bottomSheet( + hoverCardOverall.ocs!!.data!!.actions!!, + hoverCardOverall.ocs!!.data!!.displayName!!, + message.actorId!!, + context + ) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to get hover card for user " + message.actorId, e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + @SuppressLint("CheckResult") + private fun bottomSheet(actions: List, displayName: String, userId: String, context: Context) { + val filteredActions = actions.filter { allowedAppIds.contains(it.appId) } + val items = filteredActions.map { configureActionListItem(it) } + + MaterialDialog(context, BottomSheet(LayoutMode.WRAP_CONTENT)).show { + cornerRadius(res = R.dimen.corner_radius) + viewThemeUtils.material.colorBottomSheetBackground(this.view) + + title(text = displayName) + listItemsWithImage(items = items) { _, index, _ -> + + val action = filteredActions[index] + + when (AllowedAppIds.createFor(action)) { + PROFILE -> openProfile(action.hyperlink!!, context) + EMAIL -> composeEmail(action.title!!, context) + SPREED -> talkTo(userId, context) + } + } + } + } + + private fun configureActionListItem(action: HoverCardAction): BasicListItemWithImage { + val drawable = when (AllowedAppIds.createFor(action)) { + PROFILE -> R.drawable.ic_user + EMAIL -> R.drawable.ic_email + SPREED -> R.drawable.ic_talk + } + + return BasicListItemWithImage( + drawable, + action.title!! + ) + } + + private fun talkTo(userId: String, context: Context) { + val apiVersion = + ApiUtils.getConversationApiVersion(userModel, intArrayOf(ApiUtils.API_V4, 1)) + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + version = apiVersion, + baseUrl = userModel.baseUrl!!, + roomType = "1", + invite = userId + ) + val credentials = ApiUtils.getCredentials(userModel.username, userModel.token) + ncApi.createRoom( + credentials, + retrofitBucket.url, + retrofitBucket.queryMap + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(roomOverall: RoomOverall) { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token) + // bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId) + + val chatIntent = Intent(context, ChatActivity::class.java) + chatIntent.putExtras(bundle) + chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + context.startActivity(chatIntent) + } + + override fun onError(e: Throwable) { + Log.e(TAG, e.message, e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun composeEmail(address: String, context: Context) { + val addresses = arrayListOf(address) + val intent = Intent(Intent.ACTION_SENDTO).apply { + data = "mailto:".toUri() // only email apps should handle this + putExtra(Intent.EXTRA_EMAIL, addresses) + } + context.startActivity(intent) + } + + private fun openProfile(hyperlink: String, context: Context) { + val webpage: Uri = hyperlink.toUri() + val intent = Intent(Intent.ACTION_VIEW, webpage) + context.startActivity(intent) + } + + enum class AllowedAppIds(val stringValue: String) { + SPREED("spreed"), + PROFILE("profile"), + EMAIL("email"); + + companion object { + fun createFor(action: HoverCardAction): AllowedAppIds = valueOf(action.appId!!.uppercase()) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt new file mode 100644 index 0000000..1f0cf95 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt @@ -0,0 +1,151 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.app.Activity +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import autodagger.AutoInjector +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.databinding.DialogAttachmentBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.SpreedFeatures +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class AttachmentDialog(val activity: Activity, var chatActivity: ChatActivity) : BottomSheetDialog(activity) { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var dialogAttachmentBinding: DialogAttachmentBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + + dialogAttachmentBinding = DialogAttachmentBinding.inflate(layoutInflater) + setContentView(dialogAttachmentBinding.root) + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + viewThemeUtils.material.colorBottomSheetBackground(dialogAttachmentBinding.root) + viewThemeUtils.material.colorBottomSheetDragHandle(dialogAttachmentBinding.bottomSheetDragHandle) + initItemsStrings() + initItemsVisibility() + initItemsClickListeners() + } + + private fun initItemsStrings() { + var serverName = CapabilitiesUtil.getServerName(chatActivity.conversationUser) + dialogAttachmentBinding.txtAttachFileFromCloud.text = chatActivity.resources?.let { + if (serverName.isNullOrEmpty()) { + serverName = it.getString(R.string.nc_server_product_name) + } + String.format(it.getString(R.string.nc_upload_from_cloud), serverName) + } + } + + private fun initItemsVisibility() { + if (!chatActivity.currentConversation!!.remoteServer.isNullOrEmpty()) { + dialogAttachmentBinding.menuAttachContact.visibility = View.GONE + dialogAttachmentBinding.menuShareLocation.visibility = View.GONE + dialogAttachmentBinding.menuAttachPictureFromCam.visibility = View.GONE + dialogAttachmentBinding.menuAttachVideoFromCam.visibility = View.GONE + dialogAttachmentBinding.menuAttachFileFromLocal.visibility = View.GONE + dialogAttachmentBinding.menuAttachFileFromCloud.visibility = View.GONE + } + + if (!CapabilitiesUtil.hasSpreedFeatureCapability( + chatActivity.spreedCapabilities, + SpreedFeatures.GEO_LOCATION_SHARING + ) + ) { + dialogAttachmentBinding.menuShareLocation.visibility = View.GONE + } + + if (!CapabilitiesUtil.hasSpreedFeatureCapability(chatActivity.spreedCapabilities, SpreedFeatures.TALK_POLLS) || + chatActivity.isOneToOneConversation() + ) { + dialogAttachmentBinding.menuAttachPoll.visibility = View.GONE + } + + if (!CapabilitiesUtil.hasSpreedFeatureCapability( + chatActivity.spreedCapabilities, + SpreedFeatures.THREADS + ) || + chatActivity.conversationThreadId != null + ) { + dialogAttachmentBinding.menuCreateThread.visibility = View.GONE + } + + if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) { + dialogAttachmentBinding.menuAttachVideoFromCam.visibility = View.GONE + } + } + + private fun initItemsClickListeners() { + dialogAttachmentBinding.menuShareLocation.setOnClickListener { + chatActivity.showShareLocationScreen() + dismiss() + } + + dialogAttachmentBinding.menuAttachFileFromGallery.setOnClickListener { + chatActivity.showGalleryPicker() + dismiss() + } + + dialogAttachmentBinding.menuAttachFileFromLocal.setOnClickListener { + chatActivity.sendSelectLocalFileIntent() + dismiss() + } + + dialogAttachmentBinding.menuAttachPictureFromCam.setOnClickListener { + chatActivity.sendPictureFromCamIntent() + dismiss() + } + + dialogAttachmentBinding.menuAttachVideoFromCam.setOnClickListener { + chatActivity.sendVideoFromCamIntent() + dismiss() + } + + dialogAttachmentBinding.menuCreateThread.setOnClickListener { + chatActivity.createThread() + dismiss() + } + + dialogAttachmentBinding.menuAttachPoll.setOnClickListener { + chatActivity.createPoll() + dismiss() + } + + dialogAttachmentBinding.menuAttachFileFromCloud.setOnClickListener { + chatActivity.showBrowserScreen() + dismiss() + } + + dialogAttachmentBinding.menuAttachContact.setOnClickListener { + chatActivity.sendChooseContactIntent() + dismiss() + } + } + + override fun onStart() { + super.onStart() + val bottomSheet = findViewById(R.id.design_bottom_sheet) + val behavior = BottomSheetBehavior.from(bottomSheet as View) + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt new file mode 100644 index 0000000..9cdeff7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt @@ -0,0 +1,146 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.os.Bundle +import android.util.Log +import android.view.View +import android.view.ViewGroup +import autodagger.AutoInjector +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.CallActivity +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogAudioOutputBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.webrtc.WebRtcAudioManager +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(callActivity) { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var dialogAudioOutputBinding: DialogAudioOutputBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + + dialogAudioOutputBinding = DialogAudioOutputBinding.inflate(layoutInflater) + setContentView(dialogAudioOutputBinding.root) + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + viewThemeUtils.platform.themeDialogDark(dialogAudioOutputBinding.root) + updateOutputDeviceList() + initClickListeners() + } + + fun updateOutputDeviceList() { + if (callActivity.audioManager?.audioDevices?.contains(WebRtcAudioManager.AudioDevice.BLUETOOTH) == false) { + dialogAudioOutputBinding.audioOutputBluetooth.visibility = View.GONE + } else { + dialogAudioOutputBinding.audioOutputBluetooth.visibility = View.VISIBLE + } + + if (callActivity.audioManager?.audioDevices?.contains(WebRtcAudioManager.AudioDevice.EARPIECE) == false) { + dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.GONE + } else { + dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.VISIBLE + } + + if (callActivity.audioManager?.audioDevices?.contains(WebRtcAudioManager.AudioDevice.SPEAKER_PHONE) == false) { + dialogAudioOutputBinding.audioOutputSpeaker.visibility = View.GONE + } else { + dialogAudioOutputBinding.audioOutputSpeaker.visibility = View.VISIBLE + } + + if (callActivity.audioManager?.currentAudioDevice?.equals( + WebRtcAudioManager.AudioDevice.WIRED_HEADSET + ) == true + ) { + dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.GONE + dialogAudioOutputBinding.audioOutputSpeaker.visibility = View.GONE + dialogAudioOutputBinding.audioOutputWiredHeadset.visibility = View.VISIBLE + } else { + dialogAudioOutputBinding.audioOutputWiredHeadset.visibility = View.GONE + } + + highlightActiveOutputChannel() + } + + private fun highlightActiveOutputChannel() { + when (callActivity.audioManager?.currentAudioDevice) { + WebRtcAudioManager.AudioDevice.BLUETOOTH -> { + viewThemeUtils.platform.colorImageView( + dialogAudioOutputBinding.audioOutputBluetoothIcon, + ColorRole.PRIMARY + ) + viewThemeUtils.platform + .colorPrimaryTextViewElementDarkMode(dialogAudioOutputBinding.audioOutputBluetoothText) + } + + WebRtcAudioManager.AudioDevice.SPEAKER_PHONE -> { + viewThemeUtils.platform.colorImageView( + dialogAudioOutputBinding.audioOutputSpeakerIcon, + ColorRole.PRIMARY + ) + viewThemeUtils.platform + .colorPrimaryTextViewElementDarkMode(dialogAudioOutputBinding.audioOutputSpeakerText) + } + + WebRtcAudioManager.AudioDevice.EARPIECE -> { + viewThemeUtils.platform.colorImageView( + dialogAudioOutputBinding.audioOutputEarspeakerIcon, + ColorRole.PRIMARY + ) + viewThemeUtils.platform + .colorPrimaryTextViewElementDarkMode(dialogAudioOutputBinding.audioOutputEarspeakerText) + } + + WebRtcAudioManager.AudioDevice.WIRED_HEADSET -> { + viewThemeUtils.platform + .colorImageView(dialogAudioOutputBinding.audioOutputWiredHeadsetIcon, ColorRole.PRIMARY) + viewThemeUtils.platform + .colorPrimaryTextViewElementDarkMode(dialogAudioOutputBinding.audioOutputWiredHeadsetText) + } + + else -> Log.d(TAG, "AudioOutputDialog doesn't know this AudioDevice") + } + } + + private fun initClickListeners() { + dialogAudioOutputBinding.audioOutputBluetooth.setOnClickListener { + callActivity.setAudioOutputChannel(WebRtcAudioManager.AudioDevice.BLUETOOTH) + dismiss() + } + + dialogAudioOutputBinding.audioOutputSpeaker.setOnClickListener { + callActivity.setAudioOutputChannel(WebRtcAudioManager.AudioDevice.SPEAKER_PHONE) + dismiss() + } + + dialogAudioOutputBinding.audioOutputEarspeaker.setOnClickListener { + callActivity.setAudioOutputChannel(WebRtcAudioManager.AudioDevice.EARPIECE) + dismiss() + } + } + + override fun onStart() { + super.onStart() + val bottomSheet = findViewById(R.id.design_bottom_sheet) + val behavior = BottomSheetBehavior.from(bottomSheet as View) + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + companion object { + private const val TAG = "AudioOutputDialog" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java new file mode 100644 index 0000000..8bc60ff --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java @@ -0,0 +1,416 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe (dev@mhibbe.de) + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.nextcloud.talk.account.ServerSelectionActivity; +import com.nextcloud.talk.adapters.items.AdvancedUserItem; +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.conversationlist.ConversationsListActivity; +import com.nextcloud.talk.data.network.NetworkMonitor; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.databinding.DialogChooseAccountBinding; +import com.nextcloud.talk.extensions.ImageViewExtensionsKt; +import com.nextcloud.talk.invitation.data.InvitationsModel; +import com.nextcloud.talk.invitation.data.InvitationsRepository; +import com.nextcloud.talk.models.json.participants.Participant; +import com.nextcloud.talk.models.json.status.Status; +import com.nextcloud.talk.models.json.status.StatusOverall; +import com.nextcloud.talk.settings.SettingsActivity; +import com.nextcloud.talk.ui.StatusDrawable; +import com.nextcloud.talk.ui.theme.ViewThemeUtils; +import com.nextcloud.talk.users.UserManager; +import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.CapabilitiesUtil; +import com.nextcloud.talk.utils.DisplayUtils; +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew; + +import java.net.CookieManager; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import autodagger.AutoInjector; +import eu.davidea.flexibleadapter.FlexibleAdapter; +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import static com.nextcloud.talk.utils.bundle.BundleKeys.ADD_ADDITIONAL_ACCOUNT; + +@AutoInjector(NextcloudTalkApplication.class) +public class ChooseAccountDialogFragment extends DialogFragment { + public static final String TAG = ChooseAccountDialogFragment.class.getSimpleName(); + + private static final float STATUS_SIZE_IN_DP = 9f; + + Disposable disposable; + + @Inject + UserManager userManager; + + @Inject + CurrentUserProviderNew currentUserProvider; + + @Inject + CookieManager cookieManager; + + @Inject + NcApi ncApi; + + @Inject + ViewThemeUtils viewThemeUtils; + + @Inject + InvitationsRepository invitationsRepository; + + @Inject + NetworkMonitor networkMonitor; + + private DialogChooseAccountBinding binding; + private View dialogView; + + private FlexibleAdapter adapter; + private final List userItems = new ArrayList<>(); + + private Status status; + + @SuppressLint("InflateParams") + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + binding = DialogChooseAccountBinding.inflate(getLayoutInflater()); + dialogView = binding.getRoot(); + + return new MaterialAlertDialogBuilder(requireContext()).setView(dialogView).create(); + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Objects.requireNonNull(NextcloudTalkApplication.Companion.getSharedApplication()).getComponentApplication().inject(this); + User user = currentUserProvider.getCurrentUser().blockingGet(); + + themeViews(); + setupCurrentUser(user); + setupListeners(); + setupAdapter(); + networkMonitor.isOnlineLiveData().observe(this, this::prepareViews); + } + + private void setupCurrentUser(User user) { + // Defining user picture + binding.currentAccount.userIcon.setTag(""); + if (user != null) { + binding.currentAccount.userName.setText(user.getDisplayName()); + binding.currentAccount.ticker.setVisibility(View.GONE); + binding.currentAccount.account.setText((Uri.parse(user.getBaseUrl()).getHost())); + + viewThemeUtils.platform.colorImageView(binding.currentAccount.accountMenu); + + + if (user.getBaseUrl() != null && + (user.getBaseUrl().startsWith("http://") || user.getBaseUrl().startsWith("https://"))) { + binding.currentAccount.userIcon.setVisibility(View.VISIBLE); + ImageViewExtensionsKt.loadUserAvatar(binding.currentAccount.userIcon, user, user.getUserId(), true, + false); + } else { + binding.currentAccount.userIcon.setVisibility(View.INVISIBLE); + } + loadCurrentStatus(user); + } + } + + private void setupAdapter() { + if (adapter == null) { + adapter = new FlexibleAdapter<>(userItems, getActivity(), false); + + User userEntity; + + for (User userItem : userManager.getUsers().blockingGet()) { + userEntity = userItem; + Log.d(TAG, "---------------------"); + Log.d(TAG, "userEntity.getUserId() " + userEntity.getUserId()); + Log.d(TAG, "userEntity.getCurrent() " + userEntity.getCurrent()); + Log.d(TAG, "---------------------"); + + if (!userEntity.getCurrent()) { + String userId; + if (userEntity.getUserId() != null) { + userId = userEntity.getUserId(); + } else { + userId = userEntity.getUsername(); + } + + User finalUserEntity = userEntity; + invitationsRepository.fetchInvitations(userItem) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer<>() { + @Override + public void onSubscribe(Disposable d) { + disposable = d; + } + + @Override + public void onNext(InvitationsModel invitationsModel) { + addAccountToSwitcherList( + userId, + finalUserEntity, + invitationsModel.getInvitations().size() + ); + } + + @Override + public void onError(@io.reactivex.annotations.NonNull Throwable e) { + Log.e(TAG, "Failed to fetch invitations", e); + addAccountToSwitcherList( + userId, + finalUserEntity, + 0 + ); + } + + @Override + public void onComplete() { + // no actions atm + } + }); + } + } + } + } + + private void addAccountToSwitcherList( + String userId, + User finalUserEntity, + int actionsRequiredCount + ) { + Participant participant; + participant = new Participant(); + participant.setActorType(Participant.ActorType.USERS); + participant.setActorId(userId); + participant.setDisplayName(finalUserEntity.getDisplayName()); + userItems.add( + new AdvancedUserItem( + participant, + finalUserEntity, + null, + viewThemeUtils, + actionsRequiredCount + )); + adapter.addListener(onSwitchItemClickListener); + adapter.addListener(onSwitchItemLongClickListener); + adapter.updateDataSet(userItems, false); + } + + private void setupListeners() { + // Creating listeners for quick-actions + binding.currentAccount.getRoot().setOnClickListener(v -> dismiss()); + + binding.addAccount.setOnClickListener(v -> { + Intent intent = new Intent(getContext(), ServerSelectionActivity.class); + intent.putExtra(ADD_ADDITIONAL_ACCOUNT, true); + startActivity(intent); + dismiss(); + }); + binding.manageSettings.setOnClickListener(v -> { + Intent intent = new Intent(getContext(), SettingsActivity.class); + startActivity(intent); + dismiss(); + }); + + + binding.onlineStatus.setOnClickListener(v -> { + dismiss(); + if(status!= null && getActivity()!= null){ + OnlineStatusBottomDialogFragment bottomDialog = + OnlineStatusBottomDialogFragment.newInstance(status); + bottomDialog.show(requireActivity().getSupportFragmentManager(), + "fragment_online_status_bottom_dialog"); + + } + }); + binding.statusMessage.setOnClickListener(v -> { + + dismiss(); + if(status!= null && getActivity()!= null){ + StatusMessageBottomDialogFragment bottomDialog = + StatusMessageBottomDialogFragment.newInstance(status); + bottomDialog.show(getActivity().getSupportFragmentManager(), + "fragment_status_message_bottom_dialog"); + + } + }); + } + + private void themeViews() { + viewThemeUtils.platform.themeDialog(binding.getRoot()); + viewThemeUtils.platform.themeDialogDivider(binding.divider); + + viewThemeUtils.material.colorMaterialTextButton(binding.onlineStatus); + viewThemeUtils.dialog.colorDialogMenuText(binding.onlineStatus); + viewThemeUtils.material.colorMaterialTextButton(binding.statusMessage); + viewThemeUtils.dialog.colorDialogMenuText(binding.statusMessage); + viewThemeUtils.material.colorMaterialTextButton(binding.addAccount); + viewThemeUtils.dialog.colorDialogMenuText(binding.addAccount); + viewThemeUtils.material.colorMaterialTextButton(binding.manageSettings); + viewThemeUtils.dialog.colorDialogMenuText(binding.manageSettings); + } + + private void loadCurrentStatus(User user) { + String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken()); + + if (CapabilitiesUtil.isUserStatusAvailable(currentUserProvider.getCurrentUser().blockingGet())) { + binding.statusView.setVisibility(View.VISIBLE); + + ncApi.status(credentials, ApiUtils.getUrlForStatus(user.getBaseUrl())). + subscribeOn(Schedulers.io()). + observeOn(AndroidSchedulers.mainThread()). + subscribe(new Observer() { + + @Override + public void onSubscribe(@NonNull Disposable d) { + // unused atm + } + + @Override + public void onNext(@NonNull StatusOverall statusOverall) { + if (statusOverall.getOcs() != null) { + status = statusOverall.getOcs().getData(); + } + + try { + binding.onlineStatus.setEnabled(true); + binding.statusMessage.setEnabled(true); + drawStatus(); + } catch (NullPointerException npe) { + Log.i(TAG, "UI already teared down", npe); + } + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e(TAG, "Can't receive user status from server. ", e); + try { + binding.statusView.setVisibility(View.GONE); + } catch (NullPointerException npe) { + Log.i(TAG, "UI already teared down", npe); + } + } + + @Override + public void onComplete() { + // unused atm + } + }); + } + } + + private void prepareViews(Boolean isOnline) { + if (getActivity() != null) { + LinearLayoutManager layoutManager = new SmoothScrollLinearLayoutManager(getActivity()); + binding.accountsList.setLayoutManager(layoutManager); + } + binding.accountsList.setHasFixedSize(true); + binding.accountsList.setAdapter(adapter); + + if (!isOnline) { + binding.addAccount.setVisibility(View.GONE); + } + } + + public static ChooseAccountDialogFragment newInstance() { + return new ChooseAccountDialogFragment(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return dialogView; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (disposable != null && !disposable.isDisposed()) { + disposable.dispose(); + } + binding = null; + } + + private final FlexibleAdapter.OnItemClickListener onSwitchItemClickListener = + new FlexibleAdapter.OnItemClickListener() { + @Override + public boolean onItemClick(View view, int position) { + if (userItems.size() > position) { + User user = (userItems.get(position)).user; + + if (userManager.setUserAsActive(user).blockingGet()) { + cookieManager.getCookieStore().removeAll(); + + Intent intent = new Intent(getContext(), ConversationsListActivity.class); + // TODO: might be better with FLAG_ACTIVITY_SINGLE_TOP instead than FLAG_ACTIVITY_CLEAR_TOP to + // have a smoother transition. However the handling in onNewIntent() in + // ConversationListActivity must be improved for this. + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + + dismiss(); + } + } + + return true; + } + }; + + private final FlexibleAdapter.OnItemLongClickListener onSwitchItemLongClickListener = + position -> { + // do nothing. OnItemLongClickListener is necessary anyway so the activity won't handle the event + }; + + private void drawStatus() { + float size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, getContext()); + binding.currentAccount.ticker.setBackground(null); + StatusDrawable drawable = new StatusDrawable( + status.getStatus(), + status.getIcon(), + size, + 0, + getContext()); + viewThemeUtils.talk.themeStatusDrawable(binding.currentAccount.ticker.getContext(), drawable); + binding.currentAccount.ticker.setImageDrawable(drawable); + binding.currentAccount.ticker.setVisibility(View.VISIBLE); + + if (status.getMessage() != null && !status.getMessage().isEmpty()) { + binding.currentAccount.status.setText(status.getMessage()); + binding.currentAccount.status.setVisibility(View.VISIBLE); + } else { + binding.currentAccount.status.setText(""); + binding.currentAccount.status.setVisibility(View.GONE); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt new file mode 100644 index 0000000..175f6f1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt @@ -0,0 +1,161 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + + SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.net.toUri +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.LinearLayoutManager +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.adapters.items.AdvancedUserItem +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.DialogChooseAccountShareToBinding +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import java.net.CookieManager +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ChooseAccountShareToDialogFragment : DialogFragment() { + @JvmField + @Inject + var userManager: UserManager? = null + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @JvmField + @Inject + var cookieManager: CookieManager? = null + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + private var binding: DialogChooseAccountShareToBinding? = null + private var dialogView: View? = null + private var adapter: FlexibleAdapter? = null + private val userItems: MutableList = ArrayList() + + @SuppressLint("InflateParams") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogChooseAccountShareToBinding.inflate(layoutInflater) + dialogView = binding!!.root + return MaterialAlertDialogBuilder(requireContext()).setView(dialogView).create() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + val user = currentUserProvider.currentUser.blockingGet() + themeViews() + setupCurrentUser(user) + setupListeners(user) + setupAdapter() + prepareViews() + } + + private fun setupCurrentUser(user: User?) { + binding!!.currentAccount.userIcon.tag = "" + if (user != null) { + binding!!.currentAccount.userName.text = user.displayName + binding!!.currentAccount.ticker.visibility = View.GONE + binding!!.currentAccount.account.text = user.baseUrl!!.toUri().host + viewThemeUtils!!.platform.colorImageView(binding!!.currentAccount.accountMenu, ColorRole.PRIMARY) + if (user.baseUrl != null && + (user.baseUrl!!.startsWith("http://") || user.baseUrl!!.startsWith("https://")) + ) { + binding!!.currentAccount.userIcon.loadUserAvatar(user, user.userId!!, true, false) + } else { + binding!!.currentAccount.userIcon.visibility = View.INVISIBLE + } + } + } + + @Suppress("Detekt.NestedBlockDepth") + private fun setupAdapter() { + if (adapter == null) { + adapter = FlexibleAdapter(userItems, activity, false) + var userEntity: User + var participant: Participant + for (userItem in userManager!!.users.blockingGet()) { + userEntity = userItem + if (!userEntity.current) { + var userId: String? + userId = if (userEntity.userId != null) { + userEntity.userId + } else { + userEntity.username + } + participant = Participant() + participant.actorType = Participant.ActorType.USERS + participant.actorId = userId + participant.displayName = userEntity.displayName + userItems.add(AdvancedUserItem(participant, userEntity, null, viewThemeUtils, 0)) + } + } + adapter!!.addListener(onSwitchItemClickListener) + adapter!!.updateDataSet(userItems, false) + } + } + + private fun setupListeners(user: User) { + binding!!.currentAccount.root.setOnClickListener { v: View? -> dismiss() } + } + + private fun themeViews() { + viewThemeUtils!!.platform.themeDialog(binding!!.root) + } + + private fun prepareViews() { + if (activity != null) { + val layoutManager: LinearLayoutManager = SmoothScrollLinearLayoutManager(activity) + binding!!.accountsList.layoutManager = layoutManager + } + binding!!.accountsList.setHasFixedSize(true) + binding!!.accountsList.adapter = adapter + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + dialogView + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + private val onSwitchItemClickListener = FlexibleAdapter.OnItemClickListener { view, position -> + if (userItems.size > position) { + val user = userItems[position].user + if (userManager!!.setUserAsActive(user!!).blockingGet()) { + cookieManager!!.cookieStore.removeAll() + activity?.recreate() + dismiss() + } + } + true + } + + companion object { + val TAG = ChooseAccountShareToDialogFragment::class.java.simpleName + fun newInstance(): ChooseAccountShareToDialogFragment = ChooseAccountShareToDialogFragment() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ContextChatCompose.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ContextChatCompose.kt new file mode 100644 index 0000000..0da924a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ContextChatCompose.kt @@ -0,0 +1,262 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.ui.dialog + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.pm.ActivityInfo +import android.os.Bundle +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asFlow +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.data.database.mappers.asModel +import com.nextcloud.talk.models.json.chat.ChatMessageJson +import com.nextcloud.talk.ui.ComposeChatAdapter +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.bundle.BundleKeys +import javax.inject.Inject + +@Suppress("FunctionNaming", "LongMethod", "StaticFieldLeak") +class ContextChatCompose(val bundle: Bundle) { + + companion object { + const val LIMIT = 50 + const val HALF_ALPHA = 0.5f + } + + @AutoInjector(NextcloudTalkApplication::class) + inner class ContextChatComposeViewModel : ViewModel() { + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var chatViewModel: ChatViewModel + + @Inject + lateinit var userManager: UserManager + + init { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + val credentials = bundle.getString(BundleKeys.KEY_CREDENTIALS)!! + val baseUrl = bundle.getString(BundleKeys.KEY_BASE_URL)!! + val token = bundle.getString(BundleKeys.KEY_ROOM_TOKEN)!! + val messageId = bundle.getString(BundleKeys.KEY_MESSAGE_ID)!! + + chatViewModel.getContextForChatMessages(credentials, baseUrl, token, messageId, LIMIT) + } + } + + private fun Context.requireActivity(): Activity { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + throw IllegalStateException("No activity was present but it is required.") + } + + @Composable + fun GetDialogView( + shouldDismiss: MutableState, + context: Context, + contextViewModel: ContextChatComposeViewModel = ContextChatComposeViewModel() + ) { + if (shouldDismiss.value) { + context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + return + } + + context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED + val colorScheme = contextViewModel.viewThemeUtils.getColorScheme(context) + MaterialTheme(colorScheme) { + Dialog( + onDismissRequest = { + shouldDismiss.value = true + }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true, + usePlatformDefaultWidth = false + ) + ) { + Surface { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(top = 16.dp) + ) { + val user = contextViewModel.userManager.currentUser.blockingGet() + val shouldShow = !user.hasSpreedFeatureCapability("chat-get-context") || + !user.hasSpreedFeatureCapability("federation-v1") + Row( + modifier = Modifier.align(Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = { + shouldDismiss.value = true + }) { + Icon( + Icons.Filled.Close, + stringResource(R.string.close), + modifier = Modifier + .size(24.dp) + ) + } + Column(verticalArrangement = Arrangement.Center) { + val name = bundle.getString(BundleKeys.KEY_CONVERSATION_NAME)!! + Text(name, fontSize = 24.sp) + } + // Spacer(modifier = Modifier.weight(1f)) + // val cInt = context.resources.getColor(R.color.high_emphasis_text, null) + // Icon( + // painterResource(R.drawable.ic_call_black_24dp), + // "", + // tint = Color(cInt), + // modifier = Modifier + // .padding() + // .padding(end = 16.dp) + // .alpha(HALF_ALPHA) + // ) + // + // Icon( + // painterResource(R.drawable.ic_baseline_videocam_24), + // "", + // tint = Color(cInt), + // modifier = Modifier + // .padding() + // .alpha(HALF_ALPHA) + // ) + // + // ComposeChatMenu(colorScheme.background, false) + } + if (shouldShow) { + Icon( + Icons.Filled.Info, + "Info Icon", + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Text( + stringResource(R.string.nc_capabilities_failed), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } else { + val contextState = contextViewModel + .chatViewModel + .getContextChatMessages + .asFlow() + .collectAsState(listOf()) + val messagesJson = contextState.value + val messages = messagesJson.map(ChatMessageJson::asModel) + val messageId = bundle.getString(BundleKeys.KEY_MESSAGE_ID)!! + val adapter = ComposeChatAdapter(messagesJson, messageId) + SideEffect { + adapter.addMessages(messages.toMutableList(), true) + } + adapter.GetView() + } + } + } + } + } + } + + @Composable + private fun ComposeChatMenu(backgroundColor: Color, enabled: Boolean = true) { + var expanded by remember { mutableStateOf(false) } + + Box( + modifier = Modifier.wrapContentSize(Alignment.TopStart) + ) { + IconButton(onClick = { expanded = true }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = "More options" + ) + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.background(backgroundColor) + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.nc_search)) }, + onClick = { + expanded = false + }, + enabled = enabled + ) + + HorizontalDivider() + + DropdownMenuItem( + text = { Text(stringResource(R.string.nc_conversation_menu_conversation_info)) }, + onClick = { + expanded = false + }, + enabled = enabled + ) + + HorizontalDivider() + + DropdownMenuItem( + text = { Text(stringResource(R.string.nc_shared_items)) }, + onClick = { + expanded = false + }, + enabled = enabled + ) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt new file mode 100644 index 0000000..a27aa01 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt @@ -0,0 +1,457 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.MainActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.conversation.RenameConversationDialogFragment +import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel +import com.nextcloud.talk.conversationlist.ConversationsListActivity +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.DialogConversationOperationsBinding +import com.nextcloud.talk.jobs.LeaveConversationWorker +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.ShareUtils +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ConversationsListBottomDialog( + val activity: ConversationsListActivity, + val currentUser: User, + val conversation: ConversationModel +) : BottomSheetDialog(activity) { + + private lateinit var binding: DialogConversationOperationsBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var ncApiCoroutines: NcApiCoroutines + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var conversationInfoViewModel: ConversationInfoViewModel + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + lateinit var credentials: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + + binding = DialogConversationOperationsBinding.inflate(layoutInflater) + setContentView(binding.root) + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + viewThemeUtils.material.colorBottomSheetBackground(binding.root) + viewThemeUtils.material.colorBottomSheetDragHandle(binding.bottomSheetDragHandle) + initHeaderDescription() + initItemsVisibility() + initClickListeners() + + credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)!! + } + + override fun onStart() { + super.onStart() + val bottomSheet = findViewById(R.id.design_bottom_sheet) + val behavior = BottomSheetBehavior.from(bottomSheet as View) + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + private fun initHeaderDescription() { + if (!TextUtils.isEmpty(conversation.displayName)) { + binding.conversationOperationHeader.text = conversation.displayName + } else if (!TextUtils.isEmpty(conversation.name)) { + binding.conversationOperationHeader.text = conversation.name + } + } + + private fun initItemsVisibility() { + val hasFavoritesCapability = CapabilitiesUtil.hasSpreedFeatureCapability( + currentUser.capabilities?.spreedCapability!!, + SpreedFeatures.FAVORITES + ) + + binding.conversationRemoveFromFavorites.visibility = setVisibleIf( + hasFavoritesCapability && conversation.favorite + ) + binding.conversationAddToFavorites.visibility = setVisibleIf( + hasFavoritesCapability && !conversation.favorite + ) + + binding.conversationMarkAsRead.visibility = setVisibleIf( + conversation.unreadMessages > 0 && + CapabilitiesUtil.hasSpreedFeatureCapability( + currentUser.capabilities?.spreedCapability!!, + SpreedFeatures.CHAT_READ_MARKER + ) + ) + + binding.conversationMarkAsUnread.visibility = setVisibleIf( + conversation.unreadMessages <= 0 && + CapabilitiesUtil.hasSpreedFeatureCapability( + currentUser.capabilities?.spreedCapability!!, + SpreedFeatures.CHAT_UNREAD + ) + ) + + binding.conversationOperationRename.visibility = setVisibleIf( + ConversationUtils.isNameEditable(conversation, currentUser.capabilities!!.spreedCapability!!) + ) + binding.conversationLinkShare.visibility = setVisibleIf( + !ConversationUtils.isNoteToSelfConversation(conversation) + ) + + binding.conversationOperationDelete.visibility = setVisibleIf( + conversation.canDeleteConversation + ) + + binding.conversationOperationLeave.visibility = setVisibleIf( + conversation.canLeaveConversation + ) + } + + private fun setVisibleIf(boolean: Boolean): Int = + if (boolean) { + View.VISIBLE + } else { + View.GONE + } + + private fun initClickListeners() { + binding.conversationAddToFavorites.setOnClickListener { + addConversationToFavorites() + } + + binding.conversationRemoveFromFavorites.setOnClickListener { + removeConversationFromFavorites() + } + + binding.conversationMarkAsRead.setOnClickListener { + markConversationAsRead() + } + + binding.conversationMarkAsUnread.setOnClickListener { + markConversationAsUnread() + } + + binding.conversationLinkShare.setOnClickListener { + val canGeneratePrettyURL = CapabilitiesUtil.canGeneratePrettyURL(currentUser) + ShareUtils.shareConversationLink( + activity, + currentUser.baseUrl, + conversation.token, + conversation.name, + canGeneratePrettyURL + ) + dismiss() + } + + binding.conversationArchiveText.text = if (conversation.hasArchived) { + this.activity.resources.getString(R.string.unarchive_conversation) + } else { + this.activity.resources.getString(R.string.archive_conversation) + } + + binding.conversationArchive.setOnClickListener { + handleArchiving() + } + + binding.conversationOperationRename.setOnClickListener { + renameConversation() + } + + binding.conversationOperationLeave.setOnClickListener { + leaveConversation() + } + + binding.conversationOperationDelete.setOnClickListener { + deleteConversation() + } + } + + private fun handleArchiving() { + val currentUser = currentUserProvider.currentUser.blockingGet() + val token = conversation.token + lifecycleScope.launch { + if (conversation.hasArchived) { + conversationInfoViewModel.unarchiveConversation(currentUser, token) + activity.showSnackbar( + String.format( + context.resources.getString(R.string.unarchived_conversation), + conversation.displayName + ) + ) + dismiss() + } else { + conversationInfoViewModel.archiveConversation(currentUser, token) + activity.showSnackbar( + String.format( + context.resources.getString(R.string.archived_conversation), + conversation.displayName + ) + ) + dismiss() + } + } + activity.fetchRooms() + } + + @Suppress("Detekt.TooGenericExceptionCaught") + @SuppressLint("StringFormatInvalid", "TooGenericExceptionCaught") + private fun addConversationToFavorites() { + val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) + val url = ApiUtils.getUrlForRoomFavorite(apiVersion, currentUser.baseUrl!!, conversation.token) + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + ncApiCoroutines.addConversationToFavorites(credentials, url) + } + activity.fetchRooms() + activity.showSnackbar( + String.format( + context.resources.getString(R.string.added_to_favorites), + conversation.displayName + ) + ) + dismiss() + } catch (e: Exception) { + withContext(Dispatchers.Main) { + activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry)) + dismiss() + } + } + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + @SuppressLint("StringFormatInvalid", "TooGenericExceptionCaught") + private fun removeConversationFromFavorites() { + val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1)) + val url = ApiUtils.getUrlForRoomFavorite(apiVersion, currentUser.baseUrl!!, conversation.token) + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + ncApiCoroutines.removeConversationFromFavorites(credentials, url) + } + activity.fetchRooms() + activity.showSnackbar( + String.format( + context.resources.getString(R.string.removed_from_favorites), + conversation.displayName + ) + ) + dismiss() + } catch (e: Exception) { + withContext(Dispatchers.Main) { + activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry)) + dismiss() + } + } + } + } + + private fun markConversationAsUnread() { + ncApi.markRoomAsUnread( + credentials, + ApiUtils.getUrlForChatReadMarker( + chatApiVersion(), + currentUser.baseUrl!!, + conversation.token!! + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retry(1) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + @SuppressLint("StringFormatInvalid") + override fun onNext(genericOverall: GenericOverall) { + activity.fetchRooms() + activity.showSnackbar( + String.format( + context.resources.getString(R.string.marked_as_unread), + conversation.displayName + ) + ) + dismiss() + } + + override fun onError(e: Throwable) { + activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry)) + dismiss() + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun markConversationAsRead() { + val messageId = if (conversation.remoteServer.isNullOrEmpty()) { + conversation.lastMessage?.id + } else { + null + } + + ncApi.setChatReadMarker( + credentials, + ApiUtils.getUrlForChatReadMarker( + chatApiVersion(), + currentUser.baseUrl!!, + conversation.token!! + ), + messageId?.toInt() + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retry(1) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + @SuppressLint("StringFormatInvalid") + override fun onNext(genericOverall: GenericOverall) { + activity.fetchRooms() + activity.showSnackbar( + String.format( + context.resources.getString(R.string.marked_as_read), + conversation.displayName + ) + ) + dismiss() + } + + override fun onError(e: Throwable) { + activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry)) + dismiss() + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun renameConversation() { + if (!TextUtils.isEmpty(conversation.token)) { + dismiss() + val conversationDialog = RenameConversationDialogFragment.newInstance( + conversation.token!!, + conversation.displayName!! + ) + conversationDialog.show( + activity.supportFragmentManager, + TAG + ) + } + } + + @SuppressLint("StringFormatInvalid") + private fun leaveConversation() { + val dataBuilder = Data.Builder() + dataBuilder.putString(KEY_ROOM_TOKEN, conversation.token) + dataBuilder.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!) + val data = dataBuilder.build() + + val leaveConversationWorker = + OneTimeWorkRequest.Builder(LeaveConversationWorker::class.java).setInputData( + data + ).build() + WorkManager.getInstance().enqueue(leaveConversationWorker) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(leaveConversationWorker.id) + .observeForever { workInfo: WorkInfo? -> + if (workInfo != null) { + when (workInfo.state) { + WorkInfo.State.SUCCEEDED -> { + activity.showSnackbar( + String.format( + context.resources.getString(R.string.left_conversation), + conversation.displayName + ) + ) + val intent = Intent(context, MainActivity::class.java) + context.startActivity(intent) + } + + WorkInfo.State.FAILED -> { + activity.showSnackbar(context.resources.getString(R.string.nc_common_error_sorry)) + } + + else -> { + } + } + } + } + + dismiss() + } + + private fun deleteConversation() { + if (!TextUtils.isEmpty(conversation.token)) { + activity.showDeleteConversationDialog(conversation) + } + + dismiss() + } + + private fun chatApiVersion(): Int = + ApiUtils.getChatApiVersion(currentUser.capabilities!!.spreedCapability!!, intArrayOf(ApiUtils.API_V1)) + + companion object { + val TAG = ConversationsListBottomDialog::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimeCompose.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimeCompose.kt new file mode 100644 index 0000000..d1ac2a7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/DateTimeCompose.kt @@ -0,0 +1,421 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.ui.dialog + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.text.format.DateFormat +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSizeIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material3.DatePicker +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SelectableDates +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TimePicker +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberTimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.asFlow +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import java.time.DayOfWeek +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.time.temporal.TemporalAdjusters.nextOrSame +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class DateTimeCompose(val bundle: Bundle) { + private var timeState = mutableStateOf(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.MIN)) + + init { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + val user = currentUserProvider.currentUser.blockingGet() + val roomToken = bundle.getString(BundleKeys.KEY_ROOM_TOKEN)!! + val messageId = bundle.getString(BundleKeys.KEY_MESSAGE_ID)!! + val apiVersion = bundle.getInt(BundleKeys.KEY_CHAT_API_VERSION) + chatViewModel.getReminder(user, roomToken, messageId, apiVersion) + } + + @Inject + lateinit var chatViewModel: ChatViewModel + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Composable + fun GetDateTimeDialog(shouldDismiss: MutableState, context: Context) { + if (shouldDismiss.value) { + return + } + + val colorScheme = viewThemeUtils.getColorScheme(context) + val isCollapsed = remember { mutableStateOf(true) } + + MaterialTheme(colorScheme = colorScheme) { + Dialog( + onDismissRequest = { + shouldDismiss.value = true + }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true, + usePlatformDefaultWidth = isCollapsed.value + ) + ) { + Surface( + shape = RoundedCornerShape(INT_8.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .padding(INT_16.dp) + ) { + Header() + Body() + CollapsableDateTime(shouldDismiss, isCollapsed) + } + } + } + } + } + + @Composable + private fun Submission(shouldDismiss: MutableState) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + TextButton( + onClick = { + val user = currentUserProvider.currentUser.blockingGet() + val roomToken = bundle.getString(BundleKeys.KEY_ROOM_TOKEN)!! + val messageId = bundle.getString(BundleKeys.KEY_MESSAGE_ID)!! + val apiVersion = bundle.getInt(BundleKeys.KEY_CHAT_API_VERSION) + chatViewModel.deleteReminder(user, roomToken, messageId, apiVersion) + shouldDismiss.value = true + }, + modifier = Modifier + .weight(CUBED_PADDING) + ) { + Text( + stringResource(R.string.nc_delete), + color = Color.Red + ) + } + + TextButton( + onClick = { + shouldDismiss.value = true + }, + modifier = Modifier + .weight(CUBED_PADDING) + ) { + Text(stringResource(R.string.close)) + } + + TextButton( + onClick = { + val user = currentUserProvider.currentUser.blockingGet() + val roomToken = bundle.getString(BundleKeys.KEY_ROOM_TOKEN)!! + val messageId = bundle.getString(BundleKeys.KEY_MESSAGE_ID)!! + val apiVersion = bundle.getInt(BundleKeys.KEY_CHAT_API_VERSION) + val offset = timeState.value.atZone(ZoneOffset.systemDefault()).offset + val timeVal = timeState.value.toEpochSecond(offset) + chatViewModel.setReminder(user, roomToken, messageId, timeVal.toInt(), apiVersion) + shouldDismiss.value = true + }, + modifier = Modifier + .weight(CUBED_PADDING) + ) { + Text(stringResource(R.string.set)) + } + } + } + + @Composable + private fun Body() { + val context = LocalContext.current + val currTime = LocalDateTime.now() + + val timeFormatter = DateTimeFormatter.ofPattern(timePattern(context)) + val dayTimeFormatter = DateTimeFormatter.ofPattern(dayTimePattern(context)) + + val laterToday = LocalDateTime.now() + .withHour(INT_18) + .withMinute(0) + .withSecond(0) + val laterTodayStr = laterToday.format(timeFormatter) + + val tomorrow = LocalDateTime.now() + .plusDays(1) + .withHour(INT_8) + .withMinute(0) + .withSecond(0) + val tomorrowStr = tomorrow.format(dayTimeFormatter) + + val thisWeekend = LocalDateTime.now() + .with(nextOrSame(DayOfWeek.SATURDAY)) + .withHour(INT_8) + .withMinute(0) + .withSecond(0) + val thisWeekendStr = thisWeekend.format(dayTimeFormatter) + + val nextWeek = LocalDateTime.now() + .plusWeeks(1) + .with(DayOfWeek.MONDAY) + .withHour(INT_8) + .withMinute(0) + .withSecond(0) + val nextWeekStr = nextWeek.format(dayTimeFormatter) + + if (currTime < laterToday) { + TimeOption( + label = stringResource(R.string.later_today), + timeString = laterTodayStr + ) { + setTime(laterToday) + } + } + + TimeOption( + label = stringResource(R.string.tomorrow), + timeString = tomorrowStr + ) { + setTime(tomorrow) + } + + if (currTime.dayOfWeek < DayOfWeek.FRIDAY) { + TimeOption( + label = stringResource(R.string.this_weekend), + timeString = thisWeekendStr + ) { + setTime(thisWeekend) + } + } + + if (currTime.dayOfWeek != DayOfWeek.SUNDAY) { + TimeOption( + label = stringResource(R.string.next_week), + timeString = nextWeekStr + ) { + setTime(nextWeek) + } + } + + HorizontalDivider() + } + + private fun setTime(localDateTime: LocalDateTime) { + timeState.value = localDateTime + chatViewModel.overrideReminderState() + } + + @Composable + private fun Header() { + val context = LocalContext.current + Row( + modifier = Modifier + .padding(INT_8.dp) + .fillMaxWidth() + ) { + Text(stringResource(R.string.nc_remind), modifier = Modifier.weight(HALF_WEIGHT)) + + val reminderState = chatViewModel.getReminderExistState + .asFlow() + .collectAsState(ChatViewModel.GetReminderStartState) + + when (reminderState.value) { + is ChatViewModel.GetReminderExistState -> { + val timeL = + (reminderState.value as ChatViewModel.GetReminderExistState).reminder.timestamp!!.toLong() + timeState.value = LocalDateTime.ofInstant(Instant.ofEpochSecond(timeL), ZoneId.systemDefault()) + } + + else -> {} + } + + val timeText = if (timeState.value != LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.MIN)) { + timeState.value.format(DateTimeFormatter.ofPattern(fullPattern(context))) + } else { + "" + } + + Text( + timeText, + modifier = Modifier.weight(HALF_WEIGHT) + ) + } + HorizontalDivider() + } + + @SuppressLint("UnusedBoxWithConstraintsScope") + @OptIn(ExperimentalMaterial3Api::class) + @Composable + private fun CollapsableDateTime(shouldDismiss: MutableState, isCollapsed: MutableState) { + GeneralIconButton(icon = Icons.Filled.DateRange, label = "Custom") { isCollapsed.value = !isCollapsed.value } + val scrollState = rememberScrollState() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.verticalScroll(scrollState) + ) { + if (!isCollapsed.value) { + val todayMillis = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli() + val currentYear = LocalDate.now().year + val selectableDates = object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= todayMillis + + override fun isSelectableYear(year: Int): Boolean = year >= currentYear + } + + val datePickerState = rememberDatePickerState( + selectableDates = selectableDates + ) + val now = LocalDateTime.now() + val timePickerState = rememberTimePickerState( + initialHour = now.hour, + initialMinute = now.minute, + is24Hour = DateFormat.is24HourFormat(LocalContext.current) + ) + + BoxWithConstraints( + modifier = Modifier + .requiredSizeIn(minWidth = 360.dp) + ) { + val scale = remember(maxWidth) { if (maxWidth < 360.dp) maxWidth / 360.dp else 1f } + + DatePicker( + state = datePickerState, + modifier = Modifier + .scale(scale) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + TimePicker( + state = timePickerState + ) + + val date = datePickerState.selectedDateMillis?.let { + val instant = Instant.ofEpochMilli(it) + LocalDateTime.ofInstant(instant, ZoneOffset.UTC) // Google sends time in UTC + } + if (date != null) { + val year = date.year + val month = date.month + val day = date.dayOfMonth + val hour = timePickerState.hour + val minute = timePickerState.minute + val newTime = LocalDateTime.of(year, month, day, hour, minute) + setTime(newTime) + } else { + val newTime = LocalDate.now().atTime(timePickerState.hour, timePickerState.minute) + setTime(newTime) + } + } + Submission(shouldDismiss) + } + } + + @Composable + fun GeneralIconButton(icon: ImageVector, label: String, onClick: () -> Unit) { + TextButton( + onClick = onClick + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(INT_24.dp) + ) + Spacer(modifier = Modifier.width(INT_8.dp)) + Text(text = label) + } + } + } + + private fun timePattern(context: Context): String = if (DateFormat.is24HourFormat(context)) "HH:mm" else "hh:mm a" + + private fun dayTimePattern(context: Context): String = + if (DateFormat.is24HourFormat(context)) "EEE, HH:mm" else "EEE, hh:mm a" + + private fun fullPattern(context: Context): String = + if (DateFormat.is24HourFormat(context)) "dd MMM, HH:mm" else "dd MMM, hh:mm a" + + @Composable + private fun TimeOption(label: String, timeString: String, onClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(INT_8.dp) + .clickable { onClick() } + ) { + Text(label, modifier = Modifier.weight(HALF_WEIGHT)) + Text(timeString, modifier = Modifier.weight(HALF_WEIGHT)) + } + } + + companion object { + private const val HALF_WEIGHT = 0.5f + private const val INT_8 = 8 + private const val INT_16 = 16 + private const val INT_18 = 18 + private const val INT_24 = 24 + private const val CUBED_PADDING = 0.33f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt new file mode 100644 index 0000000..20789ab --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/DialogBanListFragment.kt @@ -0,0 +1,140 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.os.Bundle +import android.text.format.DateUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.BanItemListBinding +import com.nextcloud.talk.databinding.FragmentDialogBanListBinding +import com.nextcloud.talk.models.json.participants.TalkBan +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class DialogBanListFragment(val roomToken: String) : DialogFragment() { + + lateinit var binding: FragmentDialogBanListBinding + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + lateinit var viewModel: ConversationInfoViewModel + private lateinit var conversationUser: User + + private val adapter = object : BaseAdapter() { + private var bans: List = mutableListOf() + + fun setItems(items: List) { + bans = items + } + + override fun getCount(): Int = bans.size + + override fun getItem(position: Int): Any = bans[position] + + override fun getItemId(position: Int): Long = bans[position].bannedTime!!.toLong() + + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + val binding = BanItemListBinding.inflate(LayoutInflater.from(context)) + binding.banActorName.text = bans[position].bannedDisplayName + val time = bans[position].bannedTime!!.toLong() * ONE_SEC + binding.banTime.text = DateUtils.formatDateTime( + requireContext(), + time, + (DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME) + ) + binding.banReason.text = bans[position].internalNote + binding.unbanBtn.setOnClickListener { + unBanActor(bans[position].id!!.toInt()) + } + return binding.root + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + binding = FragmentDialogBanListBinding.inflate(layoutInflater) + viewModel = + ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java] + conversationUser = currentUserProvider.currentUser.blockingGet() + + themeView() + initObservers() + initListeners() + getBanList() + return binding.root + } + + private fun initObservers() { + viewModel.getTalkBanState.observe(viewLifecycleOwner) { state -> + when (state) { + is ConversationInfoViewModel.ListBansSuccessState -> { + adapter.setItems(state.talkBans) + binding.banListView.adapter = adapter + } + + is ConversationInfoViewModel.ListBansErrorState -> {} + else -> {} + } + } + + viewModel.getUnBanActorState.observe(viewLifecycleOwner) { state -> + when (state) { + is ConversationInfoViewModel.UnBanActorSuccessState -> { + getBanList() + } + + is ConversationInfoViewModel.UnBanActorErrorState -> { + Snackbar.make(binding.root, getString(R.string.error_unbanning), Snackbar.LENGTH_SHORT).show() + } + + else -> {} + } + } + } + + private fun themeView() { + viewThemeUtils.platform.colorViewBackground(binding.root) + } + + private fun initListeners() { + binding.closeBtn.setOnClickListener { dismiss() } + } + + private fun getBanList() { + viewModel.listBans(conversationUser, roomToken) + } + + private fun unBanActor(banId: Int) { + viewModel.unbanActor(conversationUser, roomToken, banId) + } + + companion object { + @JvmStatic + fun newInstance(roomToken: String) = DialogBanListFragment(roomToken) + const val ONE_SEC = 1000L + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/FileAttachmentPreviewFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/FileAttachmentPreviewFragment.kt new file mode 100644 index 0000000..c23bba0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/FileAttachmentPreviewFragment.kt @@ -0,0 +1,95 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.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 autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogFileAttachmentPreviewBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class FileAttachmentPreviewFragment : DialogFragment() { + private lateinit var files: String + private lateinit var filesList: ArrayList + private var uploadFiles: (files: MutableList, caption: String) -> Unit = { _, _ -> } + lateinit var binding: DialogFileAttachmentPreviewBinding + + @Inject + lateinit var permissionUtil: PlatformPermissionUtil + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + fun setListener(uploadFiles: (files: MutableList, caption: String) -> Unit) { + this.uploadFiles = uploadFiles + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + arguments?.let { + files = it.getString(FILE_NAMES_ARG, "") + filesList = it.getStringArrayList(FILES_TO_UPLOAD_ARG)!! + } + + binding = DialogFileAttachmentPreviewBinding.inflate(layoutInflater) + return MaterialAlertDialogBuilder(requireContext()).setView(binding.root).create() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + setUpViews() + setUpListeners() + return inflater.inflate(R.layout.dialog_file_attachment_preview, container, false) + } + + private fun setUpViews() { + binding.dialogFileAttachmentPreviewFilenames.text = files + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.buttonClose) + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.buttonSend) + viewThemeUtils.platform.colorViewBackground(binding.root) + viewThemeUtils.material.colorTextInputLayout(binding.dialogFileAttachmentPreviewLayout) + } + + private fun setUpListeners() { + binding.buttonClose.setOnClickListener { + dismiss() + } + + binding.buttonSend.setOnClickListener { + val caption: String = binding.dialogFileAttachmentPreviewCaption.text.toString() + uploadFiles(filesList, caption) + dismiss() + } + } + + companion object { + + private const val FILE_NAMES_ARG = "FILE_NAMES_ARG" + private const val FILES_TO_UPLOAD_ARG = "FILES_TO_UPLOAD_ARG" + + @JvmStatic + fun newInstance(filenames: String, filesToUpload: MutableList): FileAttachmentPreviewFragment { + val fileAttachmentFragment = FileAttachmentPreviewFragment() + val args = Bundle() + args.putString(FILE_NAMES_ARG, filenames) + args.putStringArrayList(FILES_TO_UPLOAD_ARG, ArrayList(filesToUpload)) + fileAttachmentFragment.arguments = args + return fileAttachmentFragment + } + + val TAG: String = FilterConversationFragment::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt new file mode 100644 index 0000000..c95a746 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/FilterConversationFragment.kt @@ -0,0 +1,172 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.conversationlist.ConversationsListActivity +import com.nextcloud.talk.databinding.DialogFilterConversationBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.UserIdUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class FilterConversationFragment : DialogFragment() { + lateinit var binding: DialogFilterConversationBinding + private var dialogView: View? = null + private lateinit var filterState: HashMap + + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var arbitraryStorageManager: ArbitraryStorageManager + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogFilterConversationBinding.inflate(layoutInflater) + dialogView = binding.root + filterState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arguments?.getSerializable(FILTER_STATE_ARG, HashMap::class.java) as HashMap + } else { + arguments?.getSerializable(FILTER_STATE_ARG) as HashMap + } + return MaterialAlertDialogBuilder(requireContext()).setView(dialogView).create() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + setUpColors() + setUpListeners() + return inflater.inflate(R.layout.dialog_filter_conversation, container, false) + } + + private fun setUpColors() { + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.buttonClose) + + binding.run { + listOf( + binding.root + ) + }.forEach(viewThemeUtils.platform::colorViewBackground) + + binding.run { + listOf( + unreadFilterChip, + mentionedFilterChip, + archivedFilterChip + ) + }.forEach(viewThemeUtils.talk::themeChipFilter) + + setUpChips() + } + + private fun setUpListeners() { + binding.unreadFilterChip.setOnCheckedChangeListener { _, isChecked -> + filterState[UNREAD] = isChecked + binding.unreadFilterChip.isChecked = isChecked + processSubmit() + } + + binding.mentionedFilterChip.setOnCheckedChangeListener { _, isChecked -> + filterState[MENTION] = isChecked + binding.mentionedFilterChip.isChecked = isChecked + processSubmit() + } + + binding.archivedFilterChip.setOnCheckedChangeListener { _, isChecked -> + filterState[ARCHIVE] = isChecked + binding.archivedFilterChip.isChecked = isChecked + processSubmit() + } + + binding.buttonClose.setOnClickListener { + val noFiltersActive = !( + filterState[MENTION] == true || + filterState[UNREAD] == true || + filterState[ARCHIVE] == true + ) + if (noFiltersActive) { + (requireActivity() as ConversationsListActivity).showOnlyNearFutureEvents() + } + dismiss() + } + } + + private fun setUpChips() { + binding.unreadFilterChip.isChecked = filterState[UNREAD]!! + binding.mentionedFilterChip.isChecked = filterState[MENTION]!! + + binding.archivedFilterChip.visibility = View.GONE + currentUserProvider.currentUser.blockingGet().capabilities?.spreedCapability?.let { + if (hasSpreedFeatureCapability(it, SpreedFeatures.ARCHIVE_CONVERSATIONS)) { + binding.archivedFilterChip.visibility = View.VISIBLE + binding.archivedFilterChip.isChecked = filterState[ARCHIVE]!! + } + } + } + + private fun processSubmit() { + // store + val accountId = UserIdUtils.getIdForUser(currentUserProvider.currentUser.blockingGet()) + val mentionValue = filterState[MENTION] == true + val unreadValue = filterState[UNREAD] == true + val archivedValue = filterState[ARCHIVE] == true + + arbitraryStorageManager.storeStorageSetting(accountId, MENTION, mentionValue.toString(), "") + arbitraryStorageManager.storeStorageSetting(accountId, UNREAD, unreadValue.toString(), "") + arbitraryStorageManager.storeStorageSetting(accountId, ARCHIVE, archivedValue.toString(), "") + + (requireActivity() as ConversationsListActivity).filterConversation() + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + val noFiltersActive = !( + filterState[MENTION] == true || + filterState[UNREAD] == true || + filterState[ARCHIVE] == true + ) + if (noFiltersActive) { + (requireActivity() as ConversationsListActivity).showOnlyNearFutureEvents() + } + } + + companion object { + private const val FILTER_STATE_ARG = "FILTER_STATE_ARG" + + @JvmStatic + fun newInstance(savedFilterState: MutableMap): FilterConversationFragment { + val filterConversationFragment = FilterConversationFragment() + val args = Bundle() + args.putSerializable(FILTER_STATE_ARG, HashMap(savedFilterState)) + filterConversationFragment.arguments = args + return filterConversationFragment + } + + val TAG: String = FilterConversationFragment::class.java.simpleName + const val MENTION: String = "mention" + const val UNREAD: String = "unread" + const val ARCHIVE: String = "archive" + const val DEFAULT: String = "default" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt new file mode 100644 index 0000000..5b83042 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt @@ -0,0 +1,638 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import autodagger.AutoInjector +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.network.NetworkMonitor +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.DialogMessageActionsBinding +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.domain.ReactionAddedModel +import com.nextcloud.talk.models.domain.ReactionDeletedModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.repositories.reactions.ReactionsRepository +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.ConversationUtils +import com.nextcloud.talk.utils.DateConstants +import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.SpreedFeatures +import com.vanniktech.emoji.Emoji +import com.vanniktech.emoji.EmojiPopup +import com.vanniktech.emoji.EmojiTextView +import com.vanniktech.emoji.installDisableKeyboardInput +import com.vanniktech.emoji.installForceSingleEmoji +import com.vanniktech.emoji.recent.RecentEmojiManager +import com.vanniktech.emoji.search.SearchEmojiManager +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.launch +import java.util.Date +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +@Suppress("LongParameterList", "TooManyFunctions") +class MessageActionsDialog( + private val chatActivity: ChatActivity, + private val message: ChatMessage, + private val user: User?, + private val currentConversation: ConversationModel?, + private val showMessageDeletionButton: Boolean, + private val hasChatPermission: Boolean, + private val spreedCapabilities: SpreedCapability +) : BottomSheetDialog(chatActivity) { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var reactionsRepository: ReactionsRepository + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var networkMonitor: NetworkMonitor + + private lateinit var dialogMessageActionsBinding: DialogMessageActionsBinding + + private lateinit var popup: EmojiPopup + + private val messageHasFileAttachment = + ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType() + + private val messageHasRegularText = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message + .getCalculateMessageType() && + !message.isDeleted + + private val isOlderThanTwentyFourHours = message + .createdAt + .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_EDIT_MESSAGE)) + + private val isUserAllowedToEdit = chatActivity.userAllowedByPrivilages(message) + private var isNoTimeLimitOnNoteToSelf = hasSpreedFeatureCapability( + spreedCapabilities, + SpreedFeatures + .EDIT_MESSAGES_NOTE_TO_SELF + ) && + currentConversation?.type == ConversationEnums.ConversationType.NOTE_TO_SELF + + private val isMessageBotOneToOne = (message.actorType == ACTOR_BOTS) && + ( + message.isOneToOneConversation || + message.isFormerOneToOneConversation + ) && + !isOlderThanTwentyFourHours + + private var messageIsEditable = hasSpreedFeatureCapability( + spreedCapabilities, + SpreedFeatures.EDIT_MESSAGES + ) && + messageHasRegularText && + !isOlderThanTwentyFourHours && + isUserAllowedToEdit + + private val isMessageEditable = isNoTimeLimitOnNoteToSelf || messageIsEditable || isMessageBotOneToOne + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + + dialogMessageActionsBinding = DialogMessageActionsBinding.inflate(layoutInflater) + setContentView(dialogMessageActionsBinding.root) + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + viewThemeUtils.material.colorBottomSheetBackground(dialogMessageActionsBinding.root) + viewThemeUtils.material.colorBottomSheetDragHandle(dialogMessageActionsBinding.bottomSheetDragHandle) + initEmojiBar(hasChatPermission) + initMenuItemCopy(!message.isDeleted) + initMenuItems(networkMonitor.isOnline.value) + } + + private fun initMenuItems(isOnline: Boolean) { + this.lifecycleScope.launch { + initMenuItemTranslate( + !message.isDeleted && + ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() && + CapabilitiesUtil.isTranslationsSupported(spreedCapabilities) && + isOnline + ) + initMenuEditorDetails(message.lastEditTimestamp!! != 0L && !message.isDeleted) + initMenuReplyToMessage(message.replyable && hasChatPermission) + initMenuReplyPrivately( + message.replyable && + hasUserId(user) && + hasUserActorId(message) && + currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && + isOnline + ) + initMenuOpenThread(message.isThread && chatActivity.conversationThreadId == null) + initMenuEditMessage(isMessageEditable) + initMenuDeleteMessage(showMessageDeletionButton && isOnline) + initMenuForwardMessage( + ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() && + !(message.isDeletedCommentMessage || message.isDeleted) && + isOnline + ) + initMenuRemindMessage( + !message.isDeleted && + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REMIND_ME_LATER) && + isOnline + ) + initMenuMarkAsUnread( + message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && + ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() && + isOnline + ) + initMenuShare(messageHasFileAttachment || messageHasRegularText && isOnline) + initMenuItemOpenNcApp( + ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType() && + isOnline + ) + initMenuItemSave(message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) + initMenuAddToNote( + !message.isDeleted && + !ConversationUtils.isNoteToSelfConversation(currentConversation) && + networkMonitor.isOnline.value + ) + } + } + + override fun onStart() { + super.onStart() + val bottomSheet = findViewById(R.id.design_bottom_sheet) + val behavior = BottomSheetBehavior.from(bottomSheet as View) + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + private fun hasUserId(user: User?): Boolean = user?.userId?.isNotEmpty() == true && user.userId != "?" + + private fun hasUserActorId(message: ChatMessage): Boolean = + message.user.id.startsWith("users/") && + message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId + + @SuppressLint("ClickableViewAccessibility") + private fun initEmojiMore() { + dialogMessageActionsBinding.emojiMore.setOnTouchListener { v, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + toggleEmojiPopup() + } + true + } + + popup = EmojiPopup( + rootView = dialogMessageActionsBinding.root, + editText = dialogMessageActionsBinding.emojiMore, + onEmojiPopupShownListener = { + dialogMessageActionsBinding.emojiMore.clearFocus() + dialogMessageActionsBinding.messageActions.visibility = View.GONE + }, + onEmojiClickListener = { + popup.dismiss() + clickOnEmoji(message, it.unicode) + }, + onEmojiPopupDismissListener = { + dialogMessageActionsBinding.emojiMore.clearFocus() + dialogMessageActionsBinding.messageActions.visibility = View.VISIBLE + + val imm: InputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as + InputMethodManager + imm.hideSoftInputFromWindow(dialogMessageActionsBinding.emojiMore.windowToken, 0) + } + ) + dialogMessageActionsBinding.emojiMore.installDisableKeyboardInput(popup) + dialogMessageActionsBinding.emojiMore.installForceSingleEmoji() + } + + /* + This method is a hacky workaround to avoid bug #1914 + As the bug happens only for the very first time when the popup is opened, + it is closed after some milliseconds and opened again. + */ + private fun toggleEmojiPopup() { + if (popup.isShowing) { + popup.dismiss() + } else { + popup.show() + Handler(Looper.getMainLooper()).postDelayed( + { + popup.dismiss() + popup.show() + }, + DELAY + ) + } + } + + private fun initEmojiBar(hasChatPermission: Boolean) { + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.REACTIONS) && + isPermitted(hasChatPermission) && + isReactableMessageType(message) + ) { + val recentEmojiManager = RecentEmojiManager(context, MAX_RECENTS) + val recentEmojis = recentEmojiManager.getRecentEmojis() + val searchEmojiManager = SearchEmojiManager() + + val initialSearchKeywords = listOf( + "thumbsup", + "thumbsdown", + "heart", + "joy", + "confused", + "cry", + "pray", + "fire" + ) + val initialEmojisFromSearch = mutableSetOf() + + initialSearchKeywords.forEach { keyword -> + val searchResults = searchEmojiManager.search(keyword) + if (searchResults.isNotEmpty()) { + initialEmojisFromSearch.add(searchResults[ZERO_INDEX].component1()) + recentEmojiManager.addEmoji(searchResults[ZERO_INDEX].component1()) + } + if (initialEmojisFromSearch.size >= MAX_RECENTS) { + return@forEach + } + } + val combinedEmojis = (recentEmojis + initialEmojisFromSearch).toList().distinct().take(MAX_RECENTS) + + setupEmojiView(combinedEmojis, recentEmojiManager) + + dialogMessageActionsBinding.emojiMore.setOnClickListener { + dismiss() + } + initEmojiMore() + dialogMessageActionsBinding.emojiBar.visibility = View.VISIBLE + } else { + dialogMessageActionsBinding.emojiBar.visibility = View.GONE + } + } + + private fun setupEmojiView(combinedEmojis: List, recentEmojiManager: RecentEmojiManager) { + val emojiSearchKeywords = mapOf( + "👍" to "thumbsup", + "👎" to "thumbsdown", + "❤️" to "heart", + "😂" to "joy", + "😕" to "confused", + "😢" to "cry", + "🙏" to "pray", + "🔥" to "fire" + ) + + val emojiTextViews = listOf( + dialogMessageActionsBinding.emojiThumbsUp, + dialogMessageActionsBinding.emojiThumbsDown, + dialogMessageActionsBinding.emojiHeart, + dialogMessageActionsBinding.emojiLaugh, + dialogMessageActionsBinding.emojiConfused, + dialogMessageActionsBinding.emojiCry, + dialogMessageActionsBinding.emojiPray, + dialogMessageActionsBinding.emojiFire + ) + + emojiTextViews.forEachIndexed { index, textView -> + val emoji = combinedEmojis.getOrNull(index)?.unicode + if (emoji != null) { + textView.text = emoji + checkAndSetEmojiSelfReaction(textView) + textView.setOnClickListener { + clickOnEmoji(message, emoji) + val keyword = emojiSearchKeywords[emoji] ?: "" + val result = SearchEmojiManager().search(keyword) + if (result.isNotEmpty()) { + recentEmojiManager.addEmoji(result[ZERO_INDEX].component1()) + recentEmojiManager.persist() + } + } + textView.visibility = View.VISIBLE + } else { + textView.visibility = View.GONE + } + } + } + + private fun isPermitted(hasChatPermission: Boolean): Boolean = + hasChatPermission && + ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY != + currentConversation?.conversationReadOnlyState + + private fun isReactableMessageType(message: ChatMessage): Boolean = + !(message.isCommandMessage || message.isDeletedCommentMessage || message.isDeleted) + + private fun checkAndSetEmojiSelfReaction(emoji: EmojiTextView) { + if (emoji.text?.toString() != null && message.reactionsSelf?.contains(emoji.text?.toString()) == true) { + viewThemeUtils.talk.setCheckedBackground(emoji) + } + } + + private fun initMenuMarkAsUnread(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuMarkAsUnread.setOnClickListener { + chatActivity.markAsUnread(message) + dismiss() + } + } + + dialogMessageActionsBinding.menuMarkAsUnread.visibility = getVisibility(visible) + } + + private fun initMenuForwardMessage(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuForwardMessage.setOnClickListener { + chatActivity.forwardMessage(message) + dismiss() + } + } + + dialogMessageActionsBinding.menuForwardMessage.visibility = getVisibility(visible) + } + + private fun initMenuRemindMessage(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuNotifyMessage.setOnClickListener { + chatActivity.remindMeLater(message) + dismiss() + } + } + + dialogMessageActionsBinding.menuNotifyMessage.visibility = getVisibility(visible) + } + + private fun initMenuDeleteMessage(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuDeleteMessage.setOnClickListener { + val areYouSure = context.resources.getString(R.string.message_delete_are_you_sure) + val deleteMessage = context.resources.getString(R.string.nc_delete_message) + val delete = context.resources.getString(R.string.nc_delete) + val cancel = context.resources.getString(R.string.nc_cancel) + val builder = MaterialAlertDialogBuilder(context) + builder + .setIcon( + viewThemeUtils.dialog + .colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp) + ) + .setMessage(areYouSure) + .setTitle(deleteMessage) + .setPositiveButton(delete) { dialog, which -> + chatActivity.deleteMessage(message) + dismiss() + } + .setNegativeButton(cancel) { dialog, which -> + // unused atm + } + .let { dialogBuilder -> + viewThemeUtils.dialog + .colorMaterialAlertDialogBackground(context, dialogBuilder) + } + + val dialog: AlertDialog = builder.create() + dialog.setOnShowListener { + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(BUTTON_POSITIVE), + dialog.getButton(BUTTON_NEGATIVE) + ) + } + dialog.show() + } + } + dialogMessageActionsBinding.menuDeleteMessage.visibility = getVisibility(visible) + } + + private fun initMenuEditMessage(visible: Boolean) { + dialogMessageActionsBinding.menuEditMessage.setOnClickListener { + chatActivity.messageInputViewModel.edit(message) + dismiss() + } + + dialogMessageActionsBinding.menuEditMessage.visibility = getVisibility(visible) + } + + private fun initMenuReplyPrivately(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuReplyPrivately.setOnClickListener { + chatActivity.replyPrivately(message) + dismiss() + } + } + + dialogMessageActionsBinding.menuReplyPrivately.visibility = getVisibility(visible) + } + + private fun initMenuOpenThread(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuOpenThread.setOnClickListener { + message.threadId?.let { + chatActivity.openThread(it) + dismiss() + } + } + } + + dialogMessageActionsBinding.menuOpenThread.visibility = getVisibility(visible) + } + + private fun initMenuReplyToMessage(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuReplyToMessage.setOnClickListener { + chatActivity.messageInputViewModel.reply(message) + dismiss() + } + } + + dialogMessageActionsBinding.menuReplyToMessage.visibility = getVisibility(visible) + } + + private fun initMenuEditorDetails(showEditorDetails: Boolean) { + if (showEditorDetails) { + val editedTime = dateUtils.getLocalDateTimeStringFromTimestamp( + message.lastEditTimestamp!! * + DateConstants.SECOND_DIVIDER + ) + val lastEditorName = message.lastEditActorDisplayName ?: "" + val editorName = String.format( + context.getString(R.string.message_last_edited_by), + lastEditorName + ) + dialogMessageActionsBinding.editorName.text = editorName + dialogMessageActionsBinding.editedTime.text = editedTime + } + dialogMessageActionsBinding.menuMessageEditedInfo.visibility = getVisibility(showEditorDetails) + } + + private fun initMenuItemCopy(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuCopyMessage.setOnClickListener { + chatActivity.copyMessage(message) + dismiss() + } + } + + dialogMessageActionsBinding.menuCopyMessage.visibility = getVisibility(visible) + } + + private fun initMenuItemTranslate(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuTranslateMessage.setOnClickListener { + chatActivity.translateMessage(message) + dismiss() + } + } + + dialogMessageActionsBinding.menuTranslateMessage.visibility = getVisibility(visible) + } + + private fun initMenuShare(visible: Boolean) { + if (messageHasFileAttachment) { + dialogMessageActionsBinding.menuShare.setOnClickListener { + chatActivity.checkIfSharable(message) + dismiss() + } + } + if (messageHasRegularText) { + dialogMessageActionsBinding.menuShare.setOnClickListener { + message.message?.let { messageText -> chatActivity.shareMessageText(messageText) } + dismiss() + } + } + dialogMessageActionsBinding.menuShare.visibility = getVisibility(visible) + } + + private fun initMenuItemOpenNcApp(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuOpenInNcApp.setOnClickListener { + chatActivity.openInFilesApp(message) + dismiss() + } + } + + dialogMessageActionsBinding.menuOpenInNcApp.visibility = getVisibility(visible) + } + + private fun initMenuItemSave(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuSaveMessage.setOnClickListener { + chatActivity.checkIfSaveable(message) + dismiss() + } + } + dialogMessageActionsBinding.menuSaveMessage.visibility = getVisibility(visible) + } + + private fun initMenuAddToNote(visible: Boolean) { + if (visible) { + dialogMessageActionsBinding.menuShareToNote.setOnClickListener { + chatActivity.shareToNotes(message) + dismiss() + } + } + dialogMessageActionsBinding.menuShareToNote.visibility = getVisibility(visible) + } + + private fun getVisibility(visible: Boolean): Int = + if (visible) { + View.VISIBLE + } else { + View.GONE + } + + private fun clickOnEmoji(message: ChatMessage, emoji: String) { + if (message.reactionsSelf?.contains(emoji) == true) { + reactionsRepository.deleteReaction(currentConversation!!.token!!, message, emoji) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(ReactionDeletedObserver()) + } else { + reactionsRepository.addReaction(currentConversation!!.token!!, message, emoji) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(ReactionAddedObserver()) + } + } + + inner class ReactionAddedObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(reactionAddedModel: ReactionAddedModel) { + if (reactionAddedModel.success) { + chatActivity.updateUiToAddReaction( + reactionAddedModel.chatMessage, + reactionAddedModel.emoji + ) + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failure in ReactionAddedObserver", e) + } + + override fun onComplete() { + dismiss() + } + } + + inner class ReactionDeletedObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(reactionDeletedModel: ReactionDeletedModel) { + if (reactionDeletedModel.success) { + chatActivity.updateUiToDeleteReaction( + reactionDeletedModel.chatMessage, + reactionDeletedModel.emoji + ) + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failure in ReactionDeletedObserver", e) + } + + override fun onComplete() { + dismiss() + } + } + + companion object { + private val TAG = MessageActionsDialog::class.java.simpleName + private const val ACTOR_LENGTH = 6 + private const val NO_PREVIOUS_MESSAGE_ID: Int = -1 + private const val DELAY: Long = 200 + private const val AGE_THRESHOLD_FOR_EDIT_MESSAGE: Long = 86400000 + private const val ACTOR_BOTS = "bots" + private const val ZERO_INDEX = 0 + private const val MAX_RECENTS = 8 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt new file mode 100644 index 0000000..d18e63b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt @@ -0,0 +1,190 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.os.Bundle +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.core.content.ContextCompat +import autodagger.AutoInjector +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.CallActivity +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogMoreCallActionsBinding +import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.viewmodels.CallRecordingViewModel +import com.vanniktech.emoji.EmojiTextView +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomSheetDialog(callActivity) { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var binding: DialogMoreCallActionsBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + + binding = DialogMoreCallActionsBinding.inflate(layoutInflater) + setContentView(binding.root) + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + viewThemeUtils.platform.themeDialogDark(binding.root) + + initItemsVisibility() + initEmojiBar() + initClickListeners() + initObservers() + } + + override fun onStart() { + super.onStart() + val bottomSheet = findViewById(R.id.design_bottom_sheet) + val behavior = BottomSheetBehavior.from(bottomSheet as View) + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + private fun initItemsVisibility() { + if (CapabilitiesUtil.isCallReactionsSupported(callActivity.conversationUser)) { + binding.callEmojiBar.visibility = View.VISIBLE + } else { + binding.callEmojiBar.visibility = View.GONE + } + + if (callActivity.isAllowedToStartOrStopRecording) { + binding.recordCall.visibility = View.VISIBLE + } else { + binding.recordCall.visibility = View.GONE + } + + if (callActivity.isAllowedToRaiseHand) { + binding.raiseHand.visibility = View.VISIBLE + } else { + binding.raiseHand.visibility = View.GONE + } + } + + private fun initClickListeners() { + binding.recordCall.setOnClickListener { + callActivity.callRecordingViewModel?.clickRecordButton() + } + + binding.raiseHand.setOnClickListener { + callActivity.clickRaiseOrLowerHandButton() + } + } + + private fun initEmojiBar() { + if (CapabilitiesUtil.isCallReactionsSupported(callActivity.conversationUser)) { + binding.advancedCallOptionsTitle.visibility = View.GONE + + val capabilities = callActivity.conversationUser?.capabilities + val availableReactions: ArrayList<*> = + capabilities?.spreedCapability?.config!!["call"]!!["supported-reactions"] as ArrayList<*> + + val param = LinearLayout.LayoutParams( + DisplayUtils.convertDpToPixel(EMOJI_WIDTH.toFloat(), callActivity).toInt(), + LinearLayout.LayoutParams.MATCH_PARENT + ) + + availableReactions.forEach { + val emojiView = EmojiTextView(context) + emojiView.text = it.toString() + emojiView.textSize = TEXT_SIZE + emojiView.layoutParams = param + + emojiView.setOnClickListener { view -> + callActivity.sendReaction((view as EmojiTextView).text.toString()) + dismiss() + } + binding.callEmojiBar.addView(emojiView) + } + } else { + binding.callEmojiBar.visibility = View.GONE + } + } + + private fun initObservers() { + callActivity.callRecordingViewModel?.viewState?.observe(this) { state -> + when (state) { + is CallRecordingViewModel.RecordingStoppedState, + is CallRecordingViewModel.RecordingErrorState -> { + binding.recordCallText.text = context.getText(R.string.record_start_description) + binding.recordCallIcon.setImageDrawable( + ContextCompat.getDrawable(context, R.drawable.record_start) + ) + dismiss() + } + + is CallRecordingViewModel.RecordingStartingState -> { + binding.recordCallText.text = context.getText(R.string.record_cancel_start) + binding.recordCallIcon.setImageDrawable( + ContextCompat.getDrawable(context, R.drawable.record_stop) + ) + } + + is CallRecordingViewModel.RecordingStartedState -> { + binding.recordCallText.text = context.getText(R.string.record_stop_description) + binding.recordCallIcon.setImageDrawable( + ContextCompat.getDrawable(context, R.drawable.record_stop) + ) + dismiss() + } + + is CallRecordingViewModel.RecordingStoppingState -> { + binding.recordCallText.text = context.getText(R.string.record_stopping) + } + + is CallRecordingViewModel.RecordingConfirmStopState -> { + binding.recordCallText.text = context.getText(R.string.record_stop_description) + } + + else -> { + Log.e(TAG, "unknown viewState for callRecordingViewModel") + } + } + } + + callActivity.raiseHandViewModel?.viewState?.observe(this) { state -> + when (state) { + is RaiseHandViewModel.RaisedHandState -> { + binding.raiseHandText.text = context.getText(R.string.lower_hand) + binding.raiseHandIcon.setImageDrawable( + ContextCompat.getDrawable(context, R.drawable.ic_baseline_do_not_touch_24) + ) + dismiss() + } + + is RaiseHandViewModel.LoweredHandState -> { + binding.raiseHandText.text = context.getText(R.string.raise_hand) + binding.raiseHandIcon.setImageDrawable( + ContextCompat.getDrawable(context, R.drawable.ic_hand_back_left) + ) + dismiss() + } + + else -> {} + } + } + } + + companion object { + private const val TAG = "MoreCallActionsDialog" + private const val TEXT_SIZE = 20f + private const val EMOJI_WIDTH = 40 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/OnlineStatusBottomDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/OnlineStatusBottomDialogFragment.kt new file mode 100644 index 0000000..c96541a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/OnlineStatusBottomDialogFragment.kt @@ -0,0 +1,184 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.content.res.Configuration +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.WindowInsetsControllerCompat +import autodagger.AutoInjector +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.card.MaterialCardView +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.DialogSetOnlineStatusBinding +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.status.Status +import com.nextcloud.talk.models.json.status.StatusType +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import java.util.Locale +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class OnlineStatusBottomDialogFragment : BottomSheetDialogFragment() { + private lateinit var binding: DialogSetOnlineStatusBinding + private var currentUser: User? = null + private var currentStatus: Status? = null + private val disposables: MutableList = ArrayList() + + @Inject lateinit var ncApi: NcApi + + @Inject lateinit var viewThemeUtils: ViewThemeUtils + + var currentUserProvider: CurrentUserProviderNew? = null + @Inject set + + lateinit var credentials: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + arguments?.let { + currentUser = currentUserProvider?.currentUser?.blockingGet() + currentStatus = it.getParcelable(ARG_CURRENT_STATUS_PARAM) + credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)!! + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = DialogSetOnlineStatusBinding.inflate(inflater, container, false) + viewThemeUtils.platform.themeDialog(binding.root) + viewThemeUtils.material.themeDragHandleView(binding.dragHandle) + + dialog?.window?.let { window -> + window.navigationBarColor = ContextCompat.getColor(requireContext(), R.color.bg_default) + val inLightMode = resources.configuration.uiMode and + Configuration.UI_MODE_NIGHT_MASK != Configuration.UI_MODE_NIGHT_YES + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars = inLightMode + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupGeneralStatusOptions() + currentStatus?.let { visualizeStatus(it.status) } + } + + private fun setupGeneralStatusOptions() { + binding.onlineStatus.setOnClickListener { setStatus(StatusType.ONLINE) } + binding.dndStatus.setOnClickListener { setStatus(StatusType.DND) } + binding.busyStatus.setOnClickListener { setStatus(StatusType.BUSY) } + binding.awayStatus.setOnClickListener { setStatus(StatusType.AWAY) } + binding.invisibleStatus.setOnClickListener { setStatus(StatusType.INVISIBLE) } + + viewThemeUtils.talk.themeStatusCardView(binding.onlineStatus) + viewThemeUtils.talk.themeStatusCardView(binding.dndStatus) + viewThemeUtils.talk.themeStatusCardView(binding.busyStatus) + viewThemeUtils.talk.themeStatusCardView(binding.awayStatus) + viewThemeUtils.talk.themeStatusCardView(binding.invisibleStatus) + } + + private fun setStatus(statusType: StatusType) { + visualizeStatus(statusType) + + ncApi.setStatusType(credentials, ApiUtils.getUrlForSetStatusType(currentUser?.baseUrl!!), statusType.string) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + override fun onNext(t: GenericOverall) { + dismiss() + } + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to set statusType", e) + } + override fun onComplete() { + // unused atm + } + }) + } + + private fun visualizeStatus(statusType: String) { + StatusType.entries.firstOrNull { it.name == statusType.uppercase(Locale.ROOT) }?.let { visualizeStatus(it) } + } + + private fun visualizeStatus(statusType: StatusType) { + clearTopStatus() + val views: Triple = when (statusType) { + StatusType.ONLINE -> Triple(binding.onlineStatus, binding.onlineHeadline, binding.onlineIcon) + StatusType.BUSY -> Triple(binding.busyStatus, binding.busyHeadline, binding.busyIcon) + StatusType.AWAY -> Triple(binding.awayStatus, binding.awayHeadline, binding.awayIcon) + StatusType.DND -> Triple(binding.dndStatus, binding.dndHeadline, binding.dndIcon) + StatusType.INVISIBLE -> Triple(binding.invisibleStatus, binding.invisibleHeadline, binding.invisibleIcon) + else -> return + } + views.first.isChecked = true + viewThemeUtils.platform.colorTextView(views.second, ColorRole.ON_SECONDARY_CONTAINER) + } + + private fun clearTopStatus() { + context?.let { + binding.onlineHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text, null)) + binding.awayHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text, null)) + binding.dndHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text, null)) + binding.busyHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text, null)) + binding.invisibleHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text, null)) + + binding.onlineIcon.imageTintList = null + binding.awayIcon.imageTintList = null + binding.dndIcon.imageTintList = null + binding.busyIcon.imageTintList = null + binding.invisibleIcon.imageTintList = null + + binding.onlineStatus.isChecked = false + binding.awayStatus.isChecked = false + binding.dndStatus.isChecked = false + binding.busyStatus.isChecked = false + binding.invisibleStatus.isChecked = false + } + } + + override fun onDestroyView() { + super.onDestroyView() + disposables.forEach { if (!it.isDisposed) it.dispose() } + disposables.clear() + } + + companion object { + private const val ARG_CURRENT_STATUS_PARAM = "currentStatus" + private val TAG = OnlineStatusBottomDialogFragment::class.simpleName + + @JvmStatic + fun newInstance(status: Status): OnlineStatusBottomDialogFragment { + val args = Bundle() + args.putParcelable(ARG_CURRENT_STATUS_PARAM, status) + + val fragment = OnlineStatusBottomDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt new file mode 100644 index 0000000..39e7aa8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt @@ -0,0 +1,115 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe (dev@mhibbe.de) + * SPDX-FileCopyrightText: 2023 Fariba Khandani + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.jobs.SaveFileToStorageWorker +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import java.util.concurrent.ExecutionException +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class SaveToStorageDialogFragment : DialogFragment() { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + lateinit var fileName: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + fileName = arguments?.getString(KEY_FILE_NAME)!! + } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialogText = StringBuilder() + dialogText.append(resources.getString(R.string.nc_dialog_save_to_storage_content)) + dialogText.append("\n") + dialogText.append("\n") + dialogText.append(resources.getString(R.string.nc_dialog_save_to_storage_continue)) + + val dialogBuilder = MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.nc_dialog_save_to_storage_title) + .setMessage(dialogText) + .setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { _: DialogInterface?, _: Int -> + saveImageToStorage(fileName) + } + .setNegativeButton(R.string.nc_dialog_save_to_storage_no) { _: DialogInterface?, _: Int -> + } + viewThemeUtils.dialog.colorMaterialAlertDialogBackground( + requireContext(), + dialogBuilder + ) + val dialog = dialogBuilder.show() + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ) + + return dialog + } + + @SuppressLint("LongLogTag") + private fun saveImageToStorage(fileName: String) { + val sourceFilePath = requireContext().cacheDir.path + val workerTag = SAVE_TO_STORAGE_WORKER_PREFIX + fileName + + val workers = WorkManager.getInstance(requireContext()).getWorkInfosByTag(workerTag) + try { + for (workInfo in workers.get()) { + if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { + return + } + } + } catch (e: ExecutionException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } catch (e: InterruptedException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } + + val data: Data = Data.Builder() + .putString(SaveFileToStorageWorker.KEY_FILE_NAME, fileName) + .putString(SaveFileToStorageWorker.KEY_SOURCE_FILE_PATH, "$sourceFilePath/$fileName") + .build() + + val saveWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SaveFileToStorageWorker::class.java) + .setInputData(data) + .addTag(workerTag) + .build() + + WorkManager.getInstance().enqueue(saveWorker) + } + + companion object { + val TAG = SaveToStorageDialogFragment::class.java.simpleName + private const val KEY_FILE_NAME = "keyFileName" + private const val SAVE_TO_STORAGE_WORKER_PREFIX = "saveToStorage_" + + fun newInstance(fileName: String): SaveToStorageDialogFragment { + val args = Bundle() + args.putString(KEY_FILE_NAME, fileName) + val fragment = SaveToStorageDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt new file mode 100644 index 0000000..184d91d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ScopeDialog.kt @@ -0,0 +1,80 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Tobias Kaminsky + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import autodagger.AutoInjector +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogScopeBinding +import com.nextcloud.talk.models.json.userprofile.Scope +import com.nextcloud.talk.profile.ProfileActivity +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ScopeDialog( + con: Context, + private val userInfoAdapter: ProfileActivity.UserInfoAdapter, + private val field: ProfileActivity.Field, + private val position: Int +) : BottomSheetDialog(con) { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var dialogScopeBinding: DialogScopeBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + + dialogScopeBinding = DialogScopeBinding.inflate(layoutInflater) + setContentView(dialogScopeBinding.root) + + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + viewThemeUtils.platform.themeDialog(dialogScopeBinding.root) + + if (field == ProfileActivity.Field.DISPLAYNAME || field == ProfileActivity.Field.EMAIL) { + dialogScopeBinding.scopePrivate.visibility = View.GONE + } + + dialogScopeBinding.scopePrivate.setOnClickListener { + userInfoAdapter.updateScope(position, Scope.PRIVATE) + dismiss() + } + + dialogScopeBinding.scopeLocal.setOnClickListener { + userInfoAdapter.updateScope(position, Scope.LOCAL) + dismiss() + } + + dialogScopeBinding.scopeFederated.setOnClickListener { + userInfoAdapter.updateScope(position, Scope.FEDERATED) + dismiss() + } + + dialogScopeBinding.scopePublished.setOnClickListener { + userInfoAdapter.updateScope(position, Scope.PUBLISHED) + dismiss() + } + } + + override fun onStart() { + super.onStart() + val bottomSheet = findViewById(R.id.design_bottom_sheet) + val behavior = BottomSheetBehavior.from(bottomSheet as View) + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SetPhoneNumberDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/SetPhoneNumberDialogFragment.kt new file mode 100644 index 0000000..e6913c4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SetPhoneNumberDialogFragment.kt @@ -0,0 +1,101 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Parneet Singh + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.app.Dialog +import android.content.Context +import android.content.DialogInterface +import android.content.res.ColorStateList +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import autodagger.AutoInjector +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputLayout +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogSetPhoneNumberBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class SetPhoneNumberDialogFragment : DialogFragment() { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var clickListener: SetPhoneNumberDialogClickListener + + private lateinit var binding: DialogSetPhoneNumberBinding + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogSetPhoneNumberBinding.inflate(requireActivity().layoutInflater) + + val dialogBuilder = + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.nc_settings_phone_book_integration_phone_number_dialog_title) + .setView(binding.root) + .setMessage(R.string.nc_settings_phone_book_integration_phone_number_dialog_description) + .setPositiveButton(requireContext().resources.getString(R.string.nc_common_set)) { dialog, _ -> + clickListener + .onSubmitClick(binding.phoneInputLayout, dialog) + } + .setNegativeButton(requireContext().resources.getString(R.string.nc_common_skip), null) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(requireActivity(), dialogBuilder) + + binding.phoneInputLayout.setHelperTextColor( + ColorStateList.valueOf(resources.getColor(R.color.nc_darkRed, null)) + ) + + binding.phoneEditTextField.addTextChangedListener(object : TextWatcher { + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + binding.phoneInputLayout.helperText = "" + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + // unused atm + } + override fun afterTextChanged(s: Editable?) { + // unused atm + } + }) + + return dialogBuilder.create() + } + + override fun onStart() { + super.onStart() + val alertDialog = dialog as AlertDialog? + alertDialog?.let { + viewThemeUtils.platform.colorTextButtons(it.getButton(AlertDialog.BUTTON_POSITIVE)) + viewThemeUtils.platform.colorTextButtons(it.getButton(AlertDialog.BUTTON_NEGATIVE)) + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + clickListener = context as SetPhoneNumberDialogClickListener + } + + interface SetPhoneNumberDialogClickListener { + fun onSubmitClick(textInputLayout: TextInputLayout, dialog: DialogInterface) + } + + companion object { + val TAG: String = SetPhoneNumberDialogFragment::class.java.simpleName + + fun newInstance(): DialogFragment = SetPhoneNumberDialogFragment() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt new file mode 100644 index 0000000..4c9c727 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt @@ -0,0 +1,357 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.app.Activity +import android.os.Bundle +import android.util.Log +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import autodagger.AutoInjector +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayout.OnTabSelectedListener +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.ReactionItem +import com.nextcloud.talk.adapters.ReactionItemClickListener +import com.nextcloud.talk.adapters.ReactionsAdapter +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.DialogMessageReactionsBinding +import com.nextcloud.talk.databinding.ItemReactionsTabBinding +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.reactions.ReactionsOverall +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import java.util.Collections +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ShowReactionsDialog( + val activity: Activity, + private val roomToken: String, + private val chatMessage: ChatMessage, + private val user: User?, + private val hasChatPermission: Boolean, + private val ncApi: NcApi +) : BottomSheetDialog(activity), + ReactionItemClickListener { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private lateinit var binding: DialogMessageReactionsBinding + + private var adapter: ReactionsAdapter? = null + + private val tagAll: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + + binding = DialogMessageReactionsBinding.inflate(layoutInflater) + setContentView(binding.root) + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + viewThemeUtils.material.colorBottomSheetBackground(binding.root) + viewThemeUtils.material.colorBottomSheetDragHandle(binding.bottomSheetDragHandle) + adapter = ReactionsAdapter(this, user) + binding.reactionsList.adapter = adapter + binding.reactionsList.layoutManager = LinearLayoutManager(context) + initEmojiReactions() + } + + override fun onStart() { + super.onStart() + val bottomSheet = findViewById(R.id.design_bottom_sheet) + val behavior = BottomSheetBehavior.from(bottomSheet as View) + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + private fun initEmojiReactions() { + adapter?.list?.clear() + if (chatMessage.reactions != null && chatMessage.reactions!!.isNotEmpty()) { + var reactionsTotal = 0 + for ((emoji, amount) in chatMessage.reactions!!) { + reactionsTotal = reactionsTotal.plus(amount) + val tab: TabLayout.Tab = binding.emojiReactionsTabs.newTab() // Create a new Tab names "First Tab" + + val itemBinding = ItemReactionsTabBinding.inflate(layoutInflater) + itemBinding.reactionTab.tag = emoji + itemBinding.reactionIcon.text = emoji + itemBinding.reactionCount.text = amount.toString() + tab.customView = itemBinding.root + + binding.emojiReactionsTabs.addTab(tab) + } + + val tab: TabLayout.Tab = binding.emojiReactionsTabs.newTab() // Create a new Tab names "First Tab" + + val itemBinding = ItemReactionsTabBinding.inflate(layoutInflater) + itemBinding.reactionTab.tag = tagAll + itemBinding.reactionIcon.text = context.getString(R.string.reactions_tab_all) + itemBinding.reactionCount.text = reactionsTotal.toString() + tab.customView = itemBinding.root + + binding.emojiReactionsTabs.addTab(tab, 0) + + binding.emojiReactionsTabs.getTabAt(0)?.select() + + binding.emojiReactionsTabs.addOnTabSelectedListener(object : OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + // called when a tab is reselected + updateParticipantsForEmoji(chatMessage, tab.customView?.tag as String?) + } + + override fun onTabUnselected(tab: TabLayout.Tab) { + // called when a tab is reselected + } + + override fun onTabReselected(tab: TabLayout.Tab) { + // called when a tab is reselected + } + }) + + viewThemeUtils.material.themeTabLayoutOnSurface(binding.emojiReactionsTabs) + + updateParticipantsForEmoji(chatMessage, tagAll) + } + adapter?.notifyDataSetChanged() + } + + private fun updateParticipantsForEmoji(chatMessage: ChatMessage, emoji: String?) { + adapter?.list?.clear() + + val credentials = ApiUtils.getCredentials(user?.username, user?.token) + + ncApi.getReactions( + credentials, + ApiUtils.getUrlForMessageReaction( + user?.baseUrl!!, + roomToken, + chatMessage.id + ), + emoji + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(reactionsOverall: ReactionsOverall) { + val reactionVoters: ArrayList = ArrayList() + if (reactionsOverall.ocs?.data != null) { + val map = reactionsOverall.ocs?.data + for (key in map!!.keys) { + for (reactionVoter in reactionsOverall.ocs?.data!![key]!!) { + reactionVoters.add(ReactionItem(reactionVoter, key)) + } + } + + Collections.sort(reactionVoters, ReactionComparator(user.userId)) + + adapter?.list?.addAll(reactionVoters) + adapter?.notifyDataSetChanged() + } else { + Log.e(TAG, "no voters for this reaction") + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failed to retrieve list of reaction voters") + } + + override fun onComplete() { + // unused atm + } + }) + } + + override fun onClick(reactionItem: ReactionItem) { + if (hasChatPermission && reactionItem.reactionVoter.actorId?.equals(user?.userId) == true) { + deleteReaction(chatMessage, reactionItem.reaction!!) + adapter?.list?.remove(reactionItem) + dismiss() + } + } + + private fun deleteReaction(message: ChatMessage, emoji: String) { + val credentials = ApiUtils.getCredentials(user?.username, user?.token) + ncApi.deleteReaction( + credentials, + ApiUtils.getUrlForMessageReaction( + user?.baseUrl!!, + roomToken, + message.id + ), + emoji + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(genericOverall: GenericOverall) { + Log.d(TAG, "deleted reaction: $emoji") + (activity as ChatActivity).updateUiToDeleteReaction(message, emoji) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "error while deleting reaction: $emoji") + } + + override fun onComplete() { + dismiss() + } + }) + } + + companion object { + const val TAG = "ShowReactionsDialog" + } + + class ReactionComparator(val activeUser: String?) : Comparator { + @Suppress("ReturnCount") + override fun compare(reactionItem1: ReactionItem?, reactionItem2: ReactionItem?): Int { + // sort by emoji, own account, display-name, timestamp, actor-id + + if (reactionItem1 == null && reactionItem2 == null) { + return 0 + } + if (reactionItem1 == null) { + return -1 + } + if (reactionItem2 == null) { + return 1 + } + + // emoji + val reaction = StringComparator().compare(reactionItem1.reaction, reactionItem2.reaction) + if (reaction != 0) { + return reaction + } + + // own account + val ownAccount = compareOwnAccount( + activeUser, + reactionItem1.reactionVoter.actorId, + reactionItem2.reactionVoter.actorId + ) + + if (ownAccount != 0) { + return ownAccount + } + + // display-name + val displayName = StringComparator() + .compare( + reactionItem1.reactionVoter.actorDisplayName, + reactionItem2.reactionVoter.actorDisplayName + ) + + if (displayName != 0) { + return displayName + } + + // timestamp + val timestamp = LongComparator() + .compare( + reactionItem1.reactionVoter.timestamp, + reactionItem2.reactionVoter.timestamp + ) + + if (timestamp != 0) { + return timestamp + } + + // actor-id + val actorId = StringComparator() + .compare( + reactionItem1.reactionVoter.actorId, + reactionItem2.reactionVoter.actorId + ) + + if (actorId != 0) { + return actorId + } + + return 0 + } + + @Suppress("ReturnCount") + fun compareOwnAccount(activeUser: String?, actorId1: String?, actorId2: String?): Int { + val reactionVote1Active = activeUser == actorId1 + val reactionVote2Active = activeUser == actorId2 + + if (reactionVote1Active == reactionVote2Active) { + return 0 + } + + if (activeUser == null) { + return 0 + } + + if (reactionVote1Active) { + return 1 + } + if (reactionVote2Active) { + return -1 + } + + return 0 + } + + internal class StringComparator : Comparator { + @Suppress("ReturnCount") + override fun compare(obj1: String?, obj2: String?): Int { + if (obj1 === obj2) { + return 0 + } + if (obj1 == null) { + return -1 + } + return if (obj2 == null) { + 1 + } else { + obj1.lowercase().compareTo(obj2.lowercase()) + } + } + } + + internal class LongComparator : Comparator { + @Suppress("ReturnCount") + override fun compare(obj1: Long?, obj2: Long?): Int { + if (obj1 === obj2) { + return 0 + } + if (obj1 == null) { + return -1 + } + return if (obj2 == null) { + 1 + } else { + obj1.compareTo(obj2) + } + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java b/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java new file mode 100644 index 0000000..c23a60a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SortingOrderDialogFragment.java @@ -0,0 +1,189 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Nextcloud Gmbh + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.graphics.Typeface; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.databinding.SortingOrderFragmentBinding; +import com.nextcloud.talk.ui.theme.ViewThemeUtils; +import com.nextcloud.talk.utils.FileSortOrder; +import com.nextcloud.talk.utils.preferences.AppPreferences; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import autodagger.AutoInjector; + +/** + * Dialog to show and choose the sorting order for the file listing. + */ +@AutoInjector(NextcloudTalkApplication.class) +public class SortingOrderDialogFragment extends DialogFragment implements View.OnClickListener { + + private final static String TAG = SortingOrderDialogFragment.class.getSimpleName(); + + public static final String SORTING_ORDER_FRAGMENT = "SORTING_ORDER_FRAGMENT"; + private static final String KEY_SORT_ORDER = "SORT_ORDER"; + + @Inject + AppPreferences appPreferences; + + @Inject + ViewThemeUtils viewThemeUtils; + + private SortingOrderFragmentBinding binding; + private View dialogView; + + private List taggedViews; + private String currentSortOrderName; + + public static SortingOrderDialogFragment newInstance(@NonNull FileSortOrder sortOrder) { + SortingOrderDialogFragment dialogFragment = new SortingOrderDialogFragment(); + + Bundle args = new Bundle(); + args.putString(KEY_SORT_ORDER, sortOrder.getName()); + dialogFragment.setArguments(args); + + return dialogFragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // keep the state of the fragment on configuration changes + setRetainInstance(true); + + if (getArguments() != null) { + currentSortOrderName = getArguments().getString(KEY_SORT_ORDER, + FileSortOrder.Companion.getSort_a_to_z().getName()); + } + } + + @SuppressLint("InflateParams") + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + binding = SortingOrderFragmentBinding.inflate(getLayoutInflater()); + dialogView = binding.getRoot(); + + return new MaterialAlertDialogBuilder(requireContext()).setView(dialogView).create(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return dialogView; + } + + @Override + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + + setupDialogElements(); + setupListeners(); + } + + /** + * find all relevant UI elements and set their values. + */ + private void setupDialogElements() { + viewThemeUtils.platform.themeDialog(binding.root); + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.cancel); + + taggedViews = new ArrayList<>(12); + + binding.sortByNameAscending.setTag(FileSortOrder.Companion.getSort_a_to_z()); + taggedViews.add(binding.sortByNameAscending); + binding.sortByNameAZText.setTag(FileSortOrder.Companion.getSort_a_to_z()); + taggedViews.add(binding.sortByNameAZText); + binding.sortByNameDescending.setTag(FileSortOrder.Companion.getSort_z_to_a()); + taggedViews.add(binding.sortByNameDescending); + binding.sortByNameZAText.setTag(FileSortOrder.Companion.getSort_z_to_a()); + taggedViews.add(binding.sortByNameZAText); + binding.sortByModificationDateAscending.setTag(FileSortOrder.Companion.getSort_old_to_new()); + taggedViews.add(binding.sortByModificationDateAscending); + binding.sortByModificationDateOldestFirstText.setTag(FileSortOrder.Companion.getSort_old_to_new()); + taggedViews.add(binding.sortByModificationDateOldestFirstText); + binding.sortByModificationDateDescending.setTag(FileSortOrder.Companion.getSort_new_to_old()); + taggedViews.add(binding.sortByModificationDateDescending); + binding.sortByModificationDateNewestFirstText.setTag(FileSortOrder.Companion.getSort_new_to_old()); + taggedViews.add(binding.sortByModificationDateNewestFirstText); + binding.sortBySizeAscending.setTag(FileSortOrder.Companion.getSort_small_to_big()); + taggedViews.add(binding.sortBySizeAscending); + binding.sortBySizeSmallestFirstText.setTag(FileSortOrder.Companion.getSort_small_to_big()); + taggedViews.add(binding.sortBySizeSmallestFirstText); + binding.sortBySizeDescending.setTag(FileSortOrder.Companion.getSort_big_to_small()); + taggedViews.add(binding.sortBySizeDescending); + binding.sortBySizeBiggestFirstText.setTag(FileSortOrder.Companion.getSort_big_to_small()); + taggedViews.add(binding.sortBySizeBiggestFirstText); + + setupActiveOrderSelection(); + } + + /** + * tints the icon reflecting the actual sorting choice in the apps primary color. + */ + private void setupActiveOrderSelection() { + Log.i("SortOrder", "currentSortOrderName=" + currentSortOrderName); + for (View view : taggedViews) { + Log.i("SortOrder", ((FileSortOrder) view.getTag()).getName()); + if (!((FileSortOrder) view.getTag()).getName().equals(currentSortOrderName)) { + continue; + } + if (view instanceof MaterialButton) { + viewThemeUtils.material.colorMaterialButtonText((MaterialButton) view); + } + if (view instanceof TextView) { + viewThemeUtils.platform.colorPrimaryTextViewElement((TextView) view); + ((TextView) view).setTypeface(Typeface.DEFAULT_BOLD); + } + } + } + + /** + * setup all listeners. + */ + private void setupListeners() { + binding.cancel.setOnClickListener(view -> dismiss()); + + for (View view : taggedViews) { + view.setOnClickListener(this); + } + } + + @Override + public void onDestroyView() { + Log.d(TAG, "destroy SortingOrderDialogFragment view"); + if (getDialog() != null && getRetainInstance()) { + getDialog().setDismissMessage(null); + } + binding = null; + super.onDestroyView(); + } + + @Override + public void onClick(View v) { + appPreferences.setSorting(((FileSortOrder) v.getTag()).getName()); + dismiss(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/StatusMessageBottomDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/StatusMessageBottomDialogFragment.kt new file mode 100644 index 0000000..365ccce --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/StatusMessageBottomDialogFragment.kt @@ -0,0 +1,600 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022-2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.AdapterView +import android.widget.AdapterView.OnItemSelectedListener +import android.widget.ArrayAdapter +import androidx.core.content.ContextCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.recyclerview.widget.LinearLayoutManager +import autodagger.AutoInjector +import com.bluelinelabs.logansquare.LoganSquare +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.PredefinedStatusClickListener +import com.nextcloud.talk.adapters.PredefinedStatusListAdapter +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.DialogSetStatusMessageBinding +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.status.ClearAt +import com.nextcloud.talk.models.json.status.Status +import com.nextcloud.talk.models.json.status.StatusOverall +import com.nextcloud.talk.models.json.status.predefined.PredefinedStatus +import com.nextcloud.talk.models.json.status.predefined.PredefinedStatusOverall +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.CapabilitiesUtil.isRestoreStatusAvailable +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.vanniktech.emoji.EmojiPopup +import com.vanniktech.emoji.installDisableKeyboardInput +import com.vanniktech.emoji.installForceSingleEmoji +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody +import retrofit2.HttpException +import java.util.Calendar +import java.util.Locale +import javax.inject.Inject + +private const val ARG_CURRENT_STATUS_PARAM = "currentStatus" + +private const val POS_DONT_CLEAR = 0 +private const val POS_FIFTEEN_MINUTES = 1 +private const val POS_HALF_AN_HOUR = 2 +private const val POS_AN_HOUR = 3 +private const val POS_FOUR_HOURS = 4 +private const val POS_TODAY = 5 +private const val POS_END_OF_WEEK = 6 + +private const val ONE_SECOND_IN_MILLIS = 1000 +private const val ONE_MINUTE_IN_SECONDS = 60 +private const val THIRTY_MINUTES = 30 +private const val FIFTEEN_MINUTES = 15 +private const val FOUR_HOURS = 4 +private const val LAST_HOUR_OF_DAY = 23 +private const val LAST_MINUTE_OF_HOUR = 59 +private const val LAST_SECOND_OF_MINUTE = 59 + +@AutoInjector(NextcloudTalkApplication::class) +class StatusMessageBottomDialogFragment : + BottomSheetDialogFragment(), + PredefinedStatusClickListener { + + private var selectedPredefinedStatus: PredefinedStatus? = null + + private lateinit var binding: DialogSetStatusMessageBinding + + private var currentUser: User? = null + private var currentStatus: Status? = null + private lateinit var backupStatus: Status + + val predefinedStatusesList = ArrayList() + + private val disposables: MutableList = ArrayList() + + private lateinit var adapter: PredefinedStatusListAdapter + private var clearAt: Long? = null + private lateinit var popup: EmojiPopup + private var isBackupStatusAvailable = false + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + var currentUserProvider: CurrentUserProviderNew? = null + @Inject set + + lateinit var credentials: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + arguments?.let { + currentUser = currentUserProvider?.currentUser?.blockingGet() + currentStatus = it.getParcelable(ARG_CURRENT_STATUS_PARAM) + + credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token)!! + if (isRestoreStatusAvailable(currentUser!!)) { + checkBackupStatus() + } + fetchPredefinedStatuses() + } + } + + private fun fetchPredefinedStatuses() { + ncApi.getPredefinedStatuses(credentials, ApiUtils.getUrlForPredefinedStatuses(currentUser?.baseUrl!!)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + + @SuppressLint("NotifyDataSetChanged") + override fun onNext(responseBody: ResponseBody) { + val predefinedStatusOverall: PredefinedStatusOverall = LoganSquare.parse( + responseBody.string(), + PredefinedStatusOverall::class.java + ) + predefinedStatusOverall.ocs?.data?.let { predefinedStatusesList.addAll(it) } + + if (currentStatus?.messageIsPredefined == true && currentStatus?.messageId?.isNotEmpty() == true) { + val messageId = currentStatus!!.messageId + selectedPredefinedStatus = predefinedStatusesList.firstOrNull { ps -> messageId == ps.id } + } + + adapter.notifyDataSetChanged() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error while fetching predefined statuses", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun checkBackupStatus() { + ncApi.backupStatus(credentials, ApiUtils.getUrlForBackupStatus(currentUser?.baseUrl!!, currentUser?.userId!!)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + + @SuppressLint("NotifyDataSetChanged") + override fun onNext(statusOverall: StatusOverall) { + if (statusOverall.ocs?.meta?.statusCode == HTTP_STATUS_CODE_OK) { + statusOverall.ocs?.data?.let { status -> + backupStatus = status + if (backupStatus.message != null) { + isBackupStatusAvailable = true + val backupPredefinedStatus = PredefinedStatus( + backupStatus.userId!!, + backupStatus.icon, + backupStatus.message!!, + ClearAt(type = "period", time = backupStatus.clearAt.toString()) + ) + binding.automaticStatus.visibility = View.VISIBLE + adapter.isBackupStatusAvailable = true + predefinedStatusesList.add(0, backupPredefinedStatus) + adapter.notifyDataSetChanged() + } + } + } + } + + override fun onError(e: Throwable) { + if (e is HttpException && e.code() == HTTP_STATUS_CODE_NOT_FOUND) { + Log.d(TAG, "User does not have a backup status set") + } else { + Log.e(TAG, "Error while getting user backup status", e) + } + } + + override fun onComplete() { + // unused atm + } + }) + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = DialogSetStatusMessageBinding.inflate(inflater, container, false) + viewThemeUtils.platform.themeDialog(binding.root) + viewThemeUtils.material.themeDragHandleView(binding.dragHandle) + dialog?.window?.let { window -> + window.navigationBarColor = ContextCompat.getColor(requireContext(), R.color.bg_default) + val inLightMode = resources.configuration.uiMode and + Configuration.UI_MODE_NIGHT_MASK != Configuration.UI_MODE_NIGHT_YES + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars = inLightMode + } + return binding.root + } + + @SuppressLint("DefaultLocale") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupCurrentStatus() + + adapter = PredefinedStatusListAdapter(this, requireContext(), isBackupStatusAvailable) + adapter.list = predefinedStatusesList + + binding.predefinedStatusList.adapter = adapter + binding.predefinedStatusList.layoutManager = LinearLayoutManager(context) + + if (currentStatus?.icon == null) { + binding.emoji.setText(getString(R.string.default_emoji)) + } + + binding.clearStatus.setOnClickListener { clearStatus() } + binding.setStatus.setOnClickListener { setStatusMessage() } + binding.emoji.setOnClickListener { openEmojiPopup() } + popup = EmojiPopup( + rootView = view, + editText = binding.emoji, + onEmojiClickListener = { + popup.dismiss() + binding.emoji.clearFocus() + val imm: InputMethodManager = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as + InputMethodManager + imm.hideSoftInputFromWindow(binding.emoji.windowToken, 0) + } + ) + binding.emoji.installDisableKeyboardInput(popup) + binding.emoji.installForceSingleEmoji() + + binding.clearStatusAfterSpinner.apply { + this.adapter = createClearTimesArrayAdapter() + onItemSelectedListener = object : OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { + view?.let { + setClearStatusAfterValue(position) + } + } + override fun onNothingSelected(parent: AdapterView<*>?) { + // nothing to do + } + } + } + + viewThemeUtils.platform.themeDialog(binding.root) + + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.clearStatus) + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(binding.setStatus) + + viewThemeUtils.material.colorTextInputLayout(binding.customStatusInputContainer) + } + + private fun clearStatus() { + val credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token) + ncApi.statusDeleteMessage(credentials, ApiUtils.getUrlForStatusMessage(currentUser?.baseUrl!!)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()).subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + + override fun onNext(statusOverall: GenericOverall) { + // unused atm + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to clear status", e) + } + + override fun onComplete() { + dismiss() + } + }) + } + + private fun setupCurrentStatus() { + currentStatus?.let { + binding.emoji.setText(it.icon) + binding.customStatusInput.text?.clear() + binding.customStatusInput.setText(it.message?.trim()) + + if (it.clearAt > 0) { + binding.clearStatusAfterSpinner.visibility = View.GONE + binding.remainingClearTime.apply { + binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message) + visibility = View.VISIBLE + text = DisplayUtils.getRelativeTimestamp(context, it.clearAt * ONE_SECOND_IN_MILLIS, true) + .toString() + .decapitalize(Locale.getDefault()) + setOnClickListener { + visibility = View.GONE + binding.clearStatusAfterSpinner.visibility = View.VISIBLE + binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after) + } + } + clearAt = it.clearAt + } else { + clearAt = null + } + } + } + + override fun revertStatus() { + if (isRestoreStatusAvailable(currentUser!!)) { + ncApi.revertStatus( + credentials, + ApiUtils.getUrlForRevertStatus(currentUser?.baseUrl!!, currentStatus?.messageId) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + + @SuppressLint("NotifyDataSetChanged") + override fun onNext(genericOverall: GenericOverall) { + Log.d(TAG, "$genericOverall") + if (genericOverall.ocs?.meta?.statusCode == HTTP_STATUS_CODE_OK) { + binding.automaticStatus.visibility = View.GONE + adapter.isBackupStatusAvailable = false + predefinedStatusesList.removeAt(0) + adapter.notifyDataSetChanged() + currentStatus = backupStatus + setupCurrentStatus() + dismiss() + } + } + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to revert user status", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + } + + private fun createClearTimesArrayAdapter(): ArrayAdapter { + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + adapter.add(getString(R.string.dontClear)) + adapter.add(getString(R.string.fifteenMinutes)) + adapter.add(getString(R.string.thirtyMinutes)) + adapter.add(getString(R.string.oneHour)) + adapter.add(getString(R.string.fourHours)) + adapter.add(getString(R.string.today)) + adapter.add(getString(R.string.thisWeek)) + return adapter + } + + @Suppress("ComplexMethod") + private fun setClearStatusAfterValue(item: Int) { + val currentTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + + when (item) { + POS_DONT_CLEAR -> { + // don't clear + clearAt = null + } + + POS_FIFTEEN_MINUTES -> { + clearAt = currentTime + FIFTEEN_MINUTES * ONE_MINUTE_IN_SECONDS + } + + POS_HALF_AN_HOUR -> { + // 30 minutes + clearAt = currentTime + THIRTY_MINUTES * ONE_MINUTE_IN_SECONDS + } + + POS_AN_HOUR -> { + // one hour + clearAt = currentTime + ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS + } + + POS_FOUR_HOURS -> { + // four hours + clearAt = currentTime + FOUR_HOURS * ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS + } + + POS_TODAY -> { + // today + val date = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY) + set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR) + set(Calendar.SECOND, LAST_SECOND_OF_MINUTE) + } + clearAt = date.timeInMillis / ONE_SECOND_IN_MILLIS + } + + POS_END_OF_WEEK -> { + // end of week + val date = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY) + set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR) + set(Calendar.SECOND, LAST_SECOND_OF_MINUTE) + } + + while (date.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) { + date.add(Calendar.DAY_OF_YEAR, 1) + } + + clearAt = date.timeInMillis / ONE_SECOND_IN_MILLIS + } + } + } + + private fun clearAtToUnixTime(clearAt: ClearAt?): Long { + var returnValue = -1L + + if (clearAt != null) { + if (clearAt.type == "period") { + returnValue = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + clearAt.time.toLong() + } else if (clearAt.type == "end-of") { + returnValue = clearAtToUnixTimeTypeEndOf(clearAt) + } + } + + return returnValue + } + + private fun clearAtToUnixTimeTypeEndOf(clearAt: ClearAt): Long { + var returnValue = -1L + if (clearAt.time == "day") { + val date = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY) + set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR) + set(Calendar.SECOND, LAST_SECOND_OF_MINUTE) + } + returnValue = date.timeInMillis / ONE_SECOND_IN_MILLIS + } + return returnValue + } + + private fun openEmojiPopup() { + popup.show() + } + + private fun setStatusMessage() { + val inputText = binding.customStatusInput.text.toString().ifEmpty { "" } + // The endpoint '/message/custom' expects a valid emoji as string or null + val statusIcon = binding.emoji.text.toString().ifEmpty { null } + + if (selectedPredefinedStatus == null || + selectedPredefinedStatus!!.message != inputText || + selectedPredefinedStatus!!.icon != binding.emoji.text.toString() + ) { + ncApi.setCustomStatusMessage( + credentials, + ApiUtils.getUrlForSetCustomStatus(currentUser?.baseUrl!!), + statusIcon, + inputText, + clearAt + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(t: GenericOverall) { + Log.d(TAG, "CustomStatusMessage successfully set") + dismiss() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failed to set CustomStatusMessage", e) + } + + override fun onComplete() { + // unused atm + } + }) + } else { + ncApi.setPredefinedStatusMessage( + credentials, + ApiUtils.getUrlForSetPredefinedStatus(currentUser?.baseUrl!!), + selectedPredefinedStatus!!.id, + clearAt + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread())?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + disposables.add(d) + } + + override fun onNext(t: GenericOverall) { + Log.d(TAG, "PredefinedStatusMessage successfully set") + dismiss() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failed to set PredefinedStatusMessage", e) + } + + override fun onComplete() { + // unused atm + } + }) + } + } + + override fun onClick(predefinedStatus: PredefinedStatus) { + selectedPredefinedStatus = predefinedStatus + + clearAt = clearAtToUnixTime(predefinedStatus.clearAt) + binding.emoji.setText(predefinedStatus.icon) + binding.customStatusInput.text?.clear() + binding.customStatusInput.text?.append(predefinedStatus.message) + + binding.remainingClearTime.visibility = View.GONE + binding.clearStatusAfterSpinner.visibility = View.VISIBLE + binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after) + + if (predefinedStatus.clearAt == null) { + binding.clearStatusAfterSpinner.setSelection(0) + } else { + setClearAt(predefinedStatus.clearAt!!) + } + setClearStatusAfterValue(binding.clearStatusAfterSpinner.selectedItemPosition) + } + + private fun setClearAt(clearAt: ClearAt) { + if (clearAt.type == "period") { + when (clearAt.time) { + "900" -> binding.clearStatusAfterSpinner.setSelection(POS_FIFTEEN_MINUTES) + "1800" -> binding.clearStatusAfterSpinner.setSelection(POS_HALF_AN_HOUR) + "3600" -> binding.clearStatusAfterSpinner.setSelection(POS_AN_HOUR) + "14400" -> binding.clearStatusAfterSpinner.setSelection(POS_FOUR_HOURS) + else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR) + } + } else if (clearAt.type == "end-of") { + when (clearAt.time) { + "day" -> binding.clearStatusAfterSpinner.setSelection(POS_TODAY) + "week" -> binding.clearStatusAfterSpinner.setSelection(POS_END_OF_WEEK) + else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR) + } + } + } + + private fun dispose() { + for (i in disposables.indices) { + if (!disposables[i].isDisposed) { + disposables[i].dispose() + } + } + } + + override fun onDestroy() { + dispose() + super.onDestroy() + } + + /** + * Fragment creator + */ + companion object { + private val TAG = StatusMessageBottomDialogFragment::class.simpleName + private const val HTTP_STATUS_CODE_OK = 200 + private const val HTTP_STATUS_CODE_NOT_FOUND = 404 + + @JvmStatic + fun newInstance(status: Status): StatusMessageBottomDialogFragment { + val args = Bundle() + args.putParcelable(ARG_CURRENT_STATUS_PARAM, status) + + val dialogFragment = StatusMessageBottomDialogFragment() + dialogFragment.arguments = args + return dialogFragment + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt new file mode 100644 index 0000000..23930d7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/TempMessageActionsDialog.kt @@ -0,0 +1,130 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.dialog + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import autodagger.AutoInjector +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.chat.ChatActivity +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.database.model.SendStatus +import com.nextcloud.talk.data.network.NetworkMonitor +import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DateUtils +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class TempMessageActionsDialog(private val chatActivity: ChatActivity, private val message: ChatMessage) : + BottomSheetDialog(chatActivity) { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var dateUtils: DateUtils + + @Inject + lateinit var networkMonitor: NetworkMonitor + + private lateinit var binding: DialogTempMessageActionsBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this) + + binding = DialogTempMessageActionsBinding.inflate(layoutInflater) + setContentView(binding.root) + window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + viewThemeUtils.material.colorBottomSheetBackground(binding.root) + viewThemeUtils.material.colorBottomSheetDragHandle(binding.bottomSheetDragHandle) + initMenuItems() + } + + private fun initMenuItems() { + this.lifecycleScope.launch { + val sendingFailed = message.sendStatus == SendStatus.FAILED + initResendMessage(sendingFailed && networkMonitor.isOnline.value) + initMenuEditMessage(sendingFailed || !networkMonitor.isOnline.value) + initMenuDeleteMessage(sendingFailed || !networkMonitor.isOnline.value) + initMenuItemCopy() + } + } + + override fun onStart() { + super.onStart() + val bottomSheet = findViewById(R.id.design_bottom_sheet) + val behavior = BottomSheetBehavior.from(bottomSheet as View) + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + private fun initResendMessage(visible: Boolean) { + if (visible) { + binding.menuResendMessage.setOnClickListener { + chatActivity.chatViewModel.resendMessage( + chatActivity.conversationUser!!.getCredentials(), + ApiUtils.getUrlForChat( + chatActivity.chatApiVersion, + chatActivity.conversationUser!!.baseUrl!!, + chatActivity.roomToken + ), + message + ) + dismiss() + } + } + binding.menuResendMessage.visibility = getVisibility(visible) + } + + private fun initMenuDeleteMessage(visible: Boolean) { + if (visible) { + binding.menuDeleteMessage.setOnClickListener { + chatActivity.chatViewModel.deleteTempMessage(message) + dismiss() + } + } + binding.menuDeleteMessage.visibility = getVisibility(visible) + } + + private fun initMenuEditMessage(visible: Boolean) { + if (visible) { + binding.menuEditMessage.setOnClickListener { + chatActivity.messageInputViewModel.edit(message) + dismiss() + } + } + binding.menuEditMessage.visibility = getVisibility(visible) + } + + private fun initMenuItemCopy() { + binding.menuCopyMessage.setOnClickListener { + chatActivity.copyMessage(message) + dismiss() + } + } + + private fun getVisibility(visible: Boolean): Int = + if (visible) { + View.VISIBLE + } else { + View.GONE + } + + companion object { + private val TAG = TempMessageActionsDialog::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/recyclerview/MessageSwipeActions.kt b/app/src/main/java/com/nextcloud/talk/ui/recyclerview/MessageSwipeActions.kt new file mode 100644 index 0000000..e469c4e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/recyclerview/MessageSwipeActions.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2019 Shain Singh + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Based on the MessageSwipeController by Shain Singh at: + * https://github.com/shainsingh89/SwipeToReply/blob/master/app/src/main/java/com/shain/messenger/SwipeControllerActions.kt + */ +package com.nextcloud.talk.ui.recyclerview + +/** + * Actions executed within a swipe gesture. + */ +interface MessageSwipeActions { + + /** + * Display reply message including the original, quoted message of/at [position]. + */ + fun showReplyUI(position: Int) +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/recyclerview/MessageSwipeCallback.kt b/app/src/main/java/com/nextcloud/talk/ui/recyclerview/MessageSwipeCallback.kt new file mode 100644 index 0000000..b3d2d37 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/recyclerview/MessageSwipeCallback.kt @@ -0,0 +1,276 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2019 Shain Singh + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Based on the MessageSwipeController by Shain Singh at: + * https://github.com/shainsingh89/SwipeToReply/blob/master/app/src/main/java/com/shain/messenger/MessageSwipeController.kt + */ +package com.nextcloud.talk.ui.recyclerview + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE +import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_SWIPE +import androidx.recyclerview.widget.ItemTouchHelper.RIGHT +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.min + +/** + * Callback implementation for swipe-right-gesture on messages. + * + * @property context activity's context to load resources like drawables. + * @property messageSwipeActions the actions to be executed upon swipe-right. + * @constructor Creates as swipe-right callback for messages + */ +class MessageSwipeCallback(private val context: Context, private val messageSwipeActions: MessageSwipeActions) : + ItemTouchHelper.Callback() { + + private var density = DENSITY_DEFAULT + + private lateinit var imageDrawable: Drawable + private lateinit var shareRound: Drawable + + private var currentItemViewHolder: RecyclerView.ViewHolder? = null + private lateinit var view: View + private var dX = 0f + + private var replyButtonProgress: Float = NO_PROGRESS + private var lastReplyButtonAnimationTime: Long = 0 + private var swipeBack = false + private var isVibrate = false + private var startTracking = false + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + view = viewHolder.itemView + if (viewHolder.itemView.getTag(R.string.replyable_message_view_tag) != null && + viewHolder.itemView.getTag(R.string.replyable_message_view_tag) as Boolean + ) { + imageDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_reply)!! + shareRound = AppCompatResources.getDrawable(context, R.drawable.round_bgnd)!! + return makeMovementFlags(ACTION_STATE_IDLE, RIGHT) + } + + // disable swiping any other message type + return NO_SWIPE_FLAG + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean = false + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + // unused atm + } + + override fun convertToAbsoluteDirection(flags: Int, layoutDirection: Int): Int { + if (swipeBack) { + swipeBack = false + return 0 + } + return super.convertToAbsoluteDirection(flags, layoutDirection) + } + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + if (actionState == ACTION_STATE_SWIPE) { + setTouchListener(recyclerView, viewHolder) + } + + if (view.translationX < convertToDp(SWIPE_LIMIT) || dX < this.dX) { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + this.dX = dX + startTracking = true + } + currentItemViewHolder = viewHolder + drawReplyButton(c) + } + + @SuppressLint("ClickableViewAccessibility") + private fun setTouchListener(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + recyclerView.setOnTouchListener { _, event -> + swipeBack = event.action == MotionEvent.ACTION_CANCEL || event.action == MotionEvent.ACTION_UP + if (swipeBack) { + if (abs(view.translationX) >= this@MessageSwipeCallback.convertToDp(REPLY_POINT)) { + messageSwipeActions.showReplyUI(viewHolder.adapterPosition) + } + } + false + } + } + + private fun drawReplyButton(canvas: Canvas) { + if (currentItemViewHolder == null) { + return + } + val translationX = view.translationX + val newTime = System.currentTimeMillis() + val dt = min(MIN_ANIMATION_TIME_IN_MILLIS, newTime - lastReplyButtonAnimationTime) + lastReplyButtonAnimationTime = newTime + val showing = translationX >= convertToDp(SHOW_REPLY_ICON_POINT) + if (showing) { + if (replyButtonProgress < FULL_PROGRESS) { + replyButtonProgress += dt / PROGRESS_CALCULATION_TIME_BASE + if (replyButtonProgress > FULL_PROGRESS) { + replyButtonProgress = FULL_PROGRESS + } else { + view.invalidate() + } + } + } else if (translationX <= NO_PROGRESS) { + replyButtonProgress = NO_PROGRESS + startTracking = false + isVibrate = false + } else { + if (replyButtonProgress > NO_PROGRESS) { + replyButtonProgress -= dt / PROGRESS_CALCULATION_TIME_BASE + if (replyButtonProgress < PROGRESS_THRESHOLD) { + replyButtonProgress = NO_PROGRESS + } else { + view.invalidate() + } + } + } + + val alpha: Int + val scale: Float + if (showing) { + scale = if (replyButtonProgress <= SCALE_PROGRESS_TOP_THRESHOLD) { + SCALE_PROGRESS_MULTIPLIER * (replyButtonProgress / SCALE_PROGRESS_TOP_THRESHOLD) + } else { + SCALE_PROGRESS_MULTIPLIER - + SCALE_PROGRESS_BOTTOM_THRESHOLD * + ((replyButtonProgress - SCALE_PROGRESS_TOP_THRESHOLD) / SCALE_PROGRESS_BOTTOM_THRESHOLD) + } + alpha = min(FULLY_OPAQUE, FULLY_OPAQUE * (replyButtonProgress / SCALE_PROGRESS_TOP_THRESHOLD)).toInt() + } else { + scale = replyButtonProgress + alpha = min(FULLY_OPAQUE, FULLY_OPAQUE * replyButtonProgress).toInt() + } + + if (startTracking && !isVibrate && view.translationX >= convertToDp(REPLY_POINT)) { + view.performHapticFeedback( + HapticFeedbackConstants.KEYBOARD_TAP, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING + ) + isVibrate = true + } + + drawReplyIcon(alpha, scale, canvas) + } + + private fun drawReplyIcon(alpha: Int, scale: Float, canvas: Canvas) { + val x: Int = if (view.translationX > convertToDp(SWIPE_LIMIT)) { + convertToDp(SWIPE_LIMIT) / AXIS_BASE + } else { + (view.translationX / AXIS_BASE).toInt() + } + + val y = (view.top + view.measuredHeight / AXIS_BASE).toFloat() + + shareRound.alpha = alpha + imageDrawable.alpha = alpha + + shareRound.colorFilter = PorterDuffColorFilter( + ContextCompat.getColor(context, R.color.bg_message_list_incoming_bubble), + PorterDuff.Mode.SRC_IN + ) + imageDrawable.colorFilter = PorterDuffColorFilter( + ContextCompat.getColor(context, R.color.high_emphasis_text), + PorterDuff.Mode.SRC_IN + ) + + shareRound.setBounds( + (x - convertToDp(BACKGROUND_BOUNDS_PIXEL) * scale).toInt(), + (y - convertToDp(BACKGROUND_BOUNDS_PIXEL) * scale).toInt(), + (x + convertToDp(BACKGROUND_BOUNDS_PIXEL) * scale).toInt(), + (y + convertToDp(BACKGROUND_BOUNDS_PIXEL) * scale).toInt() + ) + shareRound.draw(canvas) + + imageDrawable.setBounds( + (x - convertToDp(ICON_BOUNDS_PIXEL_LEFT) * scale).toInt(), + (y - convertToDp(ICON_BOUNDS_PIXEL_TOP) * scale).toInt(), + (x + convertToDp(ICON_BOUNDS_PIXEL_RIGHT) * scale).toInt(), + (y + convertToDp(ICON_BOUNDS_PIXEL_BOTTOM) * scale).toInt() + ) + imageDrawable.draw(canvas) + + shareRound.alpha = FULLY_OPAQUE_INT + imageDrawable.alpha = FULLY_OPAQUE_INT + } + + private fun convertToDp(pixel: Int): Int = dp(pixel.toFloat(), context) + + private fun dp(value: Float, context: Context): Int { + if (density == DENSITY_DEFAULT) { + checkDisplaySize(context) + } + return if (value == DENSITY_ZERO) { + DENSITY_ZERO_INT + } else { + ceil((density * value).toDouble()).toInt() + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun checkDisplaySize(context: Context) { + try { + density = context.resources.displayMetrics.density + } catch (e: Exception) { + Log.w(TAG, "Error calculating density", e) + } + } + + companion object { + const val TAG = "MessageSwipeCallback" + const val NO_SWIPE_FLAG: Int = 0 + const val FULLY_OPAQUE: Float = 255f + const val FULLY_OPAQUE_INT: Int = 255 + const val DENSITY_DEFAULT: Float = 1f + const val DENSITY_ZERO: Float = 0f + const val DENSITY_ZERO_INT: Int = 0 + const val REPLY_POINT: Int = 100 + const val SWIPE_LIMIT: Int = 130 + const val SHOW_REPLY_ICON_POINT: Int = 30 + const val MIN_ANIMATION_TIME_IN_MILLIS: Long = 17 + const val FULL_PROGRESS: Float = 1.0f + const val NO_PROGRESS: Float = 0.0f + const val PROGRESS_THRESHOLD: Float = 0.1f + const val PROGRESS_CALCULATION_TIME_BASE: Float = 180.0f + const val SCALE_PROGRESS_MULTIPLIER: Float = 1.2f + const val SCALE_PROGRESS_TOP_THRESHOLD: Float = 0.8f + const val SCALE_PROGRESS_BOTTOM_THRESHOLD: Float = 0.2f + const val AXIS_BASE: Int = 2 + const val BACKGROUND_BOUNDS_PIXEL: Int = 18 + const val ICON_BOUNDS_PIXEL_LEFT: Int = 12 + const val ICON_BOUNDS_PIXEL_TOP: Int = 13 + const val ICON_BOUNDS_PIXEL_RIGHT: Int = 12 + const val ICON_BOUNDS_PIXEL_BOTTOM: Int = 11 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProvider.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProvider.kt new file mode 100644 index 0000000..68c2578 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProvider.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.theme + +import com.nextcloud.android.common.ui.theme.MaterialSchemes +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.capabilities.Capabilities + +interface MaterialSchemesProvider { + fun getMaterialSchemesForUser(user: User?): MaterialSchemes + fun getMaterialSchemesForCapabilities(capabilities: Capabilities?): MaterialSchemes + fun getMaterialSchemesForCurrentUser(): MaterialSchemes +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt new file mode 100644 index 0000000..e53230e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/theme/MaterialSchemesProviderImpl.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.theme + +import com.nextcloud.android.common.ui.color.ColorUtil +import com.nextcloud.android.common.ui.theme.MaterialSchemes +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.capabilities.Capabilities +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +internal class MaterialSchemesProviderImpl @Inject constructor( + private val userProvider: CurrentUserProviderNew, + private val colorUtil: ColorUtil +) : MaterialSchemesProvider { + + private val themeCache: ConcurrentHashMap = ConcurrentHashMap() + + override fun getMaterialSchemesForUser(user: User?): MaterialSchemes { + val url: String = if (user?.baseUrl != null) { + user.baseUrl!! + } else { + FALLBACK_URL + } + + if (!themeCache.containsKey(url)) { + themeCache[url] = getMaterialSchemesForCapabilities(user?.capabilities) + } + + return themeCache[url]!! + } + + override fun getMaterialSchemesForCurrentUser(): MaterialSchemes = + getMaterialSchemesForUser(userProvider.currentUser.blockingGet()) + + override fun getMaterialSchemesForCapabilities(capabilities: Capabilities?): MaterialSchemes { + val serverTheme = ServerThemeImpl(capabilities?.themingCapability, colorUtil) + return MaterialSchemes.fromServerTheme(serverTheme) + } + + companion object { + const val FALLBACK_URL = "NULL" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/ServerThemeImpl.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/ServerThemeImpl.kt new file mode 100644 index 0000000..eab11b1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/theme/ServerThemeImpl.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.theme + +import com.nextcloud.android.common.ui.theme.ServerTheme +import com.nextcloud.talk.R +import com.nextcloud.talk.models.json.capabilities.ThemingCapability +import com.nextcloud.android.common.ui.color.ColorUtil + +internal class ServerThemeImpl(themingCapability: ThemingCapability?, colorUtil: ColorUtil) : ServerTheme { + + override val primaryColor: Int + override val colorElement: Int + override val colorElementBright: Int + override val colorElementDark: Int + override val colorText: Int + + init { + primaryColor = colorUtil.getNullSafeColorWithFallbackRes(themingCapability?.color, R.color.colorPrimary) + colorElement = colorUtil.getNullSafeColor(themingCapability?.colorElement, primaryColor) + colorElementBright = colorUtil.getNullSafeColor(themingCapability?.colorElementBright, primaryColor) + colorElementDark = colorUtil.getNullSafeColor(themingCapability?.colorElementDark, primaryColor) + colorText = colorUtil.getTextColor(themingCapability?.colorText, primaryColor) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt new file mode 100644 index 0000000..aea2703 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/theme/TalkSpecificViewThemeUtils.kt @@ -0,0 +1,454 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.theme + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.LayerDrawable +import android.text.Spannable +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.SwitchCompat +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.ViewCompat +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.materialswitch.MaterialSwitch +import com.nextcloud.android.common.ui.theme.MaterialSchemes +import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase +import com.nextcloud.android.common.ui.theme.utils.AndroidXViewThemeUtils +import com.nextcloud.android.common.ui.util.buildColorStateList +import com.nextcloud.talk.R +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding +import com.nextcloud.talk.ui.MicInputCloud +import com.nextcloud.talk.ui.StatusDrawable +import com.nextcloud.talk.ui.WaveformSeekBar +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.DrawableUtils +import com.nextcloud.talk.utils.message.MessageUtils +import com.vanniktech.emoji.EmojiTextView +import com.wooplr.spotlight.SpotlightView +import dynamiccolor.DynamicScheme +import dynamiccolor.MaterialDynamicColors +import eu.davidea.flexibleadapter.utils.FlexibleUtils +import javax.inject.Inject +import kotlin.math.roundToInt + +/** + * View theme utils specific for the Talk app. + * + */ +@Suppress("TooManyFunctions") +class TalkSpecificViewThemeUtils @Inject constructor( + schemes: MaterialSchemes, + private val appcompat: AndroidXViewThemeUtils +) : ViewThemeUtilsBase(schemes) { + private val dynamicColor = MaterialDynamicColors() + fun themeIncomingMessageBubble(bubble: View, grouped: Boolean, deleted: Boolean, isPlayed: Boolean = false) { + val resources = bubble.resources + + var bubbleResource = R.drawable.shape_incoming_message + + if (grouped) { + bubbleResource = R.drawable.shape_grouped_incoming_message + } + + val bgBubbleColor = if (deleted) { + resources.getColor(R.color.bg_message_list_incoming_bubble_deleted, null) + } else if (isPlayed) { + resources.getColor(R.color.bg_message_list_incoming_bubble_audio_played, null) + } else { + resources.getColor(R.color.bg_message_list_incoming_bubble, null) + } + val bubbleDrawable = DisplayUtils.getMessageSelector( + bgBubbleColor, + resources.getColor(R.color.transparent, null), + bgBubbleColor, + bubbleResource + ) + ViewCompat.setBackground(bubble, bubbleDrawable) + } + + fun themeOutgoingMessageBubble(bubble: View, grouped: Boolean, deleted: Boolean, isPlayed: Boolean = false) { + withScheme(bubble) { scheme -> + val bgBubbleColor = if (deleted) { + ColorUtils.setAlphaComponent(dynamicColor.surfaceVariant().getArgb(scheme), HALF_ALPHA_INT) + } else if (isPlayed) { + ContextCompat.getColor(bubble.context, R.color.bg_message_list_outgoing_bubble_audio_played) + } else { + dynamicColor.surfaceVariant().getArgb(scheme) + } + + val layout = if (grouped) { + R.drawable.shape_grouped_outcoming_message + } else { + R.drawable.shape_outcoming_message + } + val bubbleDrawable = DisplayUtils.getMessageSelector( + bgBubbleColor, + ResourcesCompat.getColor(bubble.resources, R.color.transparent, null), + bgBubbleColor, + layout + ) + ViewCompat.setBackground(bubble, bubbleDrawable) + } + } + + fun colorOutgoingQuoteText(textView: TextView) { + withScheme(textView) { scheme -> + textView.setTextColor(dynamicColor.onSurfaceVariant().getArgb(scheme)) + } + } + + fun colorOutgoingQuoteAuthorText(textView: TextView) { + withScheme(textView) { scheme -> + ColorUtils.setAlphaComponent(dynamicColor.onSurfaceVariant().getArgb(scheme), ALPHA_80_INT) + } + } + + fun colorContactChatItemName(contactName: androidx.emoji2.widget.EmojiTextView) { + withScheme(contactName) { scheme -> + contactName.setTextColor(dynamicColor.onPrimaryContainer().getArgb(scheme)) + } + } + + fun colorContactChatItemBackground(card: MaterialCardView) { + withScheme(card) { scheme -> + card.setCardBackgroundColor(dynamicColor.primaryContainer().getArgb(scheme)) + } + } + + fun colorSwitch(preference: MaterialSwitch) { + val switch = preference as SwitchCompat + appcompat.colorSwitchCompat(switch) + } + + fun setCheckedBackground(emoji: EmojiTextView) { + withScheme(emoji) { scheme -> + val drawable = AppCompatResources + .getDrawable(emoji.context, R.drawable.reaction_self_bottom_sheet_background)!! + .mutate() + DrawableCompat.setTintList( + drawable, + ColorStateList.valueOf(dynamicColor.primary().getArgb(scheme)) + ) + emoji.background = drawable + } + } + + fun setCheckedBackground(linearLayout: LinearLayout, outgoing: Boolean, isBubbled: Boolean) { + withScheme(linearLayout) { scheme -> + val drawable = AppCompatResources + .getDrawable(linearLayout.context, R.drawable.reaction_self_background)!! + .mutate() + val backgroundColor = if (outgoing && isBubbled) { + ContextCompat.getColor( + linearLayout.context, + R.color.bg_message_list_incoming_bubble + ) + } else { + dynamicColor.primaryContainer().getArgb(scheme) + } + DrawableCompat.setTintList( + drawable, + ColorStateList.valueOf(backgroundColor) + ) + linearLayout.background = drawable + } + } + + fun getPlaceholderImage(context: Context, mimetype: String?): Drawable? { + val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(mimetype) + val drawable = AppCompatResources.getDrawable( + context, + drawableResourceId + ) + if (drawable != null && THEMEABLE_PLACEHOLDER_IDS.contains(drawableResourceId)) { + colorDrawable(context, drawable) + } + return drawable + } + + private fun colorDrawable(context: Context, drawable: Drawable) { + withScheme(context) { scheme -> + drawable.setTint(dynamicColor.primary().getArgb(scheme)) + } + } + + fun themePlaceholderAvatar(avatar: View, @DrawableRes foreground: Int): Drawable? { + var drawable: LayerDrawable? = null + withScheme(avatar) { scheme -> + val layers = arrayOfNulls(2) + layers[0] = ContextCompat.getDrawable(avatar.context, R.drawable.ic_avatar_background) + layers[0]?.setTint(dynamicColor.surfaceVariant().getArgb(scheme)) + layers[1] = ContextCompat.getDrawable(avatar.context, foreground) + layers[1]?.setTint(dynamicColor.onSurfaceVariant().getArgb(scheme)) + drawable = LayerDrawable(layers) + } + + return drawable + } + + @SuppressLint("RestrictedApi") + fun themeSearchView(searchView: SearchView) { + withScheme(searchView) { scheme -> + // hacky as no default way is provided + val editText = searchView.findViewById(R.id.search_src_text) + val searchPlate = searchView.findViewById(R.id.search_plate) + editText.setHintTextColor(dynamicColor.onSurfaceVariant().getArgb(scheme)) + editText.setTextColor(dynamicColor.onSurface().getArgb(scheme)) + editText.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) + searchPlate.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) + } + } + + fun themeStatusCardView(cardView: MaterialCardView) { + withScheme(cardView) { scheme -> + cardView.backgroundTintList = + ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_checked) + ), + intArrayOf( + dynamicColor.secondaryContainer().getArgb(scheme), + dynamicColor.surfaceContainerHigh().getArgb(scheme) + ) + ) + cardView.setStrokeColor( + ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_checked) + ), + intArrayOf( + dynamicColor.onSecondaryContainer().getArgb(scheme), + dynamicColor.surface().getArgb(scheme) + ) + ) + ) + } + } + + fun themeMicInputCloud(micInputCloud: MicInputCloud) { + withScheme(micInputCloud) { scheme -> + micInputCloud.setColor(dynamicColor.primary().getArgb(scheme)) + } + } + + fun themeWaveFormSeekBar(waveformSeekBar: WaveformSeekBar) { + withScheme(waveformSeekBar) { scheme -> + waveformSeekBar.thumb.colorFilter = + PorterDuffColorFilter(dynamicColor.inversePrimary().getArgb(scheme), PorterDuff.Mode.SRC_IN) + waveformSeekBar.setColors( + dynamicColor.inversePrimary().getArgb(scheme), + dynamicColor.onPrimaryContainer().getArgb(scheme) + ) + waveformSeekBar.progressDrawable?.colorFilter = + PorterDuffColorFilter(dynamicColor.primary().getArgb(scheme), PorterDuff.Mode.SRC_IN) + } + } + + fun themeForegroundColorSpan(context: Context): ForegroundColorSpan { + return withScheme(context) { scheme -> + return@withScheme ForegroundColorSpan(dynamicColor.primary().getArgb(scheme)) + } + } + + fun themeSpotlightView(context: Context, builder: SpotlightView.Builder): SpotlightView.Builder { + return withScheme(context) { scheme -> + return@withScheme builder.headingTvColor(dynamicColor.primary().getArgb(scheme)) + .lineAndArcColor(dynamicColor.primary().getArgb(scheme)) + } + } + + fun themeAndHighlightText(textView: TextView, originalText: String?, c: String?) { + withScheme(textView) { scheme -> + var constraint = c + constraint = FlexibleUtils.toLowerCase(constraint) + var start = FlexibleUtils.toLowerCase(originalText).indexOf(constraint) + if (start != -1) { + val spanText = Spannable.Factory.getInstance().newSpannable(originalText) + do { + val end = start + constraint.length + spanText.setSpan( + ForegroundColorSpan(dynamicColor.primary().getArgb(scheme)), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + spanText.setSpan(StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + start = FlexibleUtils.toLowerCase(originalText) + .indexOf(constraint, end + 1) // +1 skips the consecutive span + } while (start != -1) + textView.setText(spanText, TextView.BufferType.SPANNABLE) + } else { + textView.setText(originalText, TextView.BufferType.NORMAL) + } + } + } + + fun themeSortButton(sortButton: MaterialButton) { + withScheme(sortButton) { scheme -> + sortButton.iconTint = ColorStateList.valueOf(dynamicColor.onSurface().getArgb(scheme)) + sortButton.setTextColor(dynamicColor.onSurface().getArgb(scheme)) + } + } + + fun themePathNavigationButton(navigationBtn: MaterialButton) { + withScheme(navigationBtn) { scheme -> + navigationBtn.iconTint = ColorStateList.valueOf(dynamicColor.onSurface().getArgb(scheme)) + navigationBtn.setTextColor(dynamicColor.onSurface().getArgb(scheme)) + } + } + + fun themeSortListButtonGroup(relativeLayout: RelativeLayout) { + withScheme(relativeLayout) { scheme -> + relativeLayout.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) + } + } + + fun themeStatusDrawable(context: Context, statusDrawable: StatusDrawable) { + withScheme(context) { scheme -> + statusDrawable.colorStatusDrawable(dynamicColor.surface().getArgb(scheme)) + } + } + + fun themeMessageCheckMark(imageView: ImageView) { + withScheme(imageView) { scheme -> + imageView.setColorFilter( + dynamicColor.onSurfaceVariant().getArgb(scheme), + PorterDuff.Mode.SRC_ATOP + ) + } + } + + fun themeMarkdown(context: Context, message: String, incoming: Boolean): Spanned { + return withScheme(context) { scheme -> + return@withScheme if (incoming) { + MessageUtils(context).getRenderedMarkdownText( + context, + message, + context.getColor(R.color.nc_incoming_text_default) + ) + } else { + MessageUtils(context).getRenderedMarkdownText( + context, + message, + dynamicColor.onSurfaceVariant().getArgb(scheme) + ) + } + } + } + + fun themeParentMessage( + parentChatMessage: ChatMessage, + message: ChatMessage, + quoteColoredView: View, + @ColorRes quoteColorNonSelf: Int = R.color.textColorMaxContrast + ) { + withScheme(quoteColoredView) { scheme -> + val shapeRectangle = ContextCompat.getDrawable( + quoteColoredView.context, + R.drawable.reply_background + ) + as LayerDrawable + val gradient = shapeRectangle.findDrawableByLayerId(R.id.quoteLine) as GradientDrawable + if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { + gradient.setColor(dynamicColor.primary().getArgb(scheme)) + } else { + gradient.setColor(dynamicColor.onSurfaceVariant().getArgb(scheme)) + } + quoteColoredView.background = shapeRectangle + } + } + + fun getTextColor(isOutgoingMessage: Boolean, isSelfReaction: Boolean, binding: ReactionsInsideMessageBinding): Int { + return withScheme(binding.root) { scheme -> + return@withScheme if (!isOutgoingMessage || isSelfReaction) { + ContextCompat.getColor(binding.root.context, R.color.high_emphasis_text) + } else { + dynamicColor.onSurfaceVariant().getArgb(scheme) + } + } + } + + private fun chipOutlineFilterColorList(scheme: DynamicScheme) = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.secondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.outline().getArgb(scheme) + ) + + fun themeChipFilter(chip: Chip) { + withScheme(chip.context) { scheme -> + val backgroundColors = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.secondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.surface().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.secondaryContainer().getArgb(scheme), + android.R.attr.state_hovered to dynamicColor.secondaryContainer().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.secondaryContainer().getArgb(scheme) + ) + + val iconColors = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.surfaceVariant().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.onSecondaryContainer().getArgb(scheme), + android.R.attr.state_hovered to dynamicColor.onSecondaryContainer().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.onSecondaryContainer().getArgb(scheme) + ) + + val textColors = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), + android.R.attr.state_hovered to dynamicColor.onSecondaryContainer().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.onSecondaryContainer().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.onSecondaryContainer().getArgb(scheme) + ) + + chip.chipBackgroundColor = backgroundColors + chip.chipStrokeColor = chipOutlineFilterColorList(scheme) + chip.setTextColor(textColors) + chip.checkedIconTint = iconColors + } + } + + companion object { + private val THEMEABLE_PLACEHOLDER_IDS = listOf( + R.drawable.ic_mimetype_package_x_generic, + R.drawable.ic_mimetype_folder + ) + + private val ALPHA_80_INT: Int = (255 * 0.8).roundToInt() + + private const val HALF_ALPHA_INT: Int = 255 / 2 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt new file mode 100644 index 0000000..296c34c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/theme/ThemeModule.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.theme + +import com.nextcloud.android.common.ui.theme.MaterialSchemes +import com.nextcloud.talk.dagger.modules.ContextModule +import com.nextcloud.talk.utils.database.user.UserModule +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.Reusable + +@Module(includes = [ContextModule::class, UserModule::class]) +internal abstract class ThemeModule { + + @Binds + @Reusable + abstract fun bindMaterialSchemesProvider(provider: MaterialSchemesProviderImpl): MaterialSchemesProvider + + companion object { + @Provides + fun provideCurrentMaterialSchemes(schemesProvider: MaterialSchemesProvider): MaterialSchemes = + schemesProvider.getMaterialSchemesForCurrentUser() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtils.kt b/app/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtils.kt new file mode 100644 index 0000000..21d6d6b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/ui/theme/ViewThemeUtils.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.ui.theme + +import com.nextcloud.android.common.ui.theme.MaterialSchemes +import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase +import com.nextcloud.android.common.ui.theme.utils.AndroidViewThemeUtils +import com.nextcloud.android.common.ui.theme.utils.AndroidXViewThemeUtils +import com.nextcloud.android.common.ui.theme.utils.DialogViewThemeUtils +import com.nextcloud.android.common.ui.theme.utils.MaterialViewThemeUtils +import javax.inject.Inject + +@Suppress("TooManyFunctions") +class ViewThemeUtils @Inject constructor( + schemes: MaterialSchemes, + @JvmField + val platform: AndroidViewThemeUtils, + @JvmField + val material: MaterialViewThemeUtils, + @JvmField + val androidx: AndroidXViewThemeUtils, + @JvmField + val talk: TalkSpecificViewThemeUtils, + @JvmField + val dialog: DialogViewThemeUtils +) : ViewThemeUtilsBase(schemes) diff --git a/app/src/main/java/com/nextcloud/talk/upload/chunked/Chunk.kt b/app/src/main/java/com/nextcloud/talk/upload/chunked/Chunk.kt new file mode 100644 index 0000000..896c484 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/upload/chunked/Chunk.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2019 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.upload.chunked + +data class Chunk(var start: Long, var end: Long) { + fun length(): Long = end - start + 1 +} diff --git a/app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkFromFileRequestBody.kt b/app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkFromFileRequestBody.kt new file mode 100644 index 0000000..10a0c33 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkFromFileRequestBody.kt @@ -0,0 +1,102 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-FileCopyrightText: 2020 ownCloud GmbH + * SPDX-License-Identifier: MIT + */ +package com.nextcloud.talk.upload.chunked + +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.BufferedSink +import java.io.File +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.channels.FileChannel + +/** + * A Request body that represents a file chunk and include information about the progress when uploading it + * + * @author David González Verdugo + */ +class ChunkFromFileRequestBody( + file: File, + contentType: MediaType?, + channel: FileChannel?, + chunkSize: Long, + offset: Long, + listener: OnDataTransferProgressListener +) : RequestBody() { + private val mFile: File + private val mContentType: MediaType? + private val mChannel: FileChannel + private val mChunkSize: Long + private val mOffset: Long + private var mTransferred: Long + private var mDataTransferListener: OnDataTransferProgressListener + private val mBuffer = ByteBuffer.allocate(BUFFER_CAPACITY) + override fun contentLength(): Long = + try { + mChunkSize.coerceAtMost(mChannel.size() - mOffset) + } catch (e: IOException) { + mChunkSize + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + var readCount: Int + try { + mChannel.position(mOffset) + var size = mFile.length() + if (size == 0L) { + size = -1 + } + val maxCount = (mOffset + mChunkSize - 1).coerceAtMost(mChannel.size()) + var percentageOld = 0 + while (mChannel.position() < maxCount) { + readCount = mChannel.read(mBuffer) + sink.buffer.write(mBuffer.array(), 0, readCount) + mBuffer.clear() + if (mTransferred < maxCount) { // condition to avoid accumulate progress for repeated chunks + mTransferred += readCount.toLong() + } + + val percentage = + if (size > ZERO_PERCENT) (mTransferred * HUNDRED_PERCENT / size).toInt() else ZERO_PERCENT + if (percentage > percentageOld) { + percentageOld = percentage + mDataTransferListener.onTransferProgress( + percentage + ) + } + } + } catch (io: IOException) { + // any read problem will be handled as if the file is not there + val fnf = java.io.FileNotFoundException("Exception reading source file") + fnf.initCause(io) + throw fnf + } + } + + override fun contentType(): MediaType? = mContentType + + companion object { + private val TAG = ChunkFromFileRequestBody::class.java.simpleName + private const val BUFFER_CAPACITY = 4096 + private const val HUNDRED_PERCENT = 100 + private const val ZERO_PERCENT = 0 + } + + init { + requireNotNull(channel) { "File may not be null" } + require(chunkSize > 0) { "Chunk size must be greater than zero" } + mFile = file + mChannel = channel + mChunkSize = chunkSize + mOffset = offset + mTransferred = offset + mDataTransferListener = listener + mContentType = contentType + } +} diff --git a/app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt b/app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt new file mode 100644 index 0000000..6419e20 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/upload/chunked/ChunkedFileUploader.kt @@ -0,0 +1,396 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-FileCopyrightText: 2015 ownCloud GmbH + * SPDX-License-Identifier: MIT + */ +package com.nextcloud.talk.upload.chunked + +import android.net.Uri +import android.text.TextUtils +import android.util.Log +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.exception.DavException +import at.bitfire.dav4jvm.exception.HttpException +import at.bitfire.dav4jvm.property.DisplayName +import at.bitfire.dav4jvm.property.GetContentType +import at.bitfire.dav4jvm.property.GetLastModified +import at.bitfire.dav4jvm.property.ResourceType +import autodagger.AutoInjector +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.filebrowser.models.DavResponse +import com.nextcloud.talk.filebrowser.models.properties.NCEncrypted +import com.nextcloud.talk.filebrowser.models.properties.NCPermission +import com.nextcloud.talk.filebrowser.models.properties.NCPreview +import com.nextcloud.talk.filebrowser.models.properties.OCFavorite +import com.nextcloud.talk.filebrowser.models.properties.OCId +import com.nextcloud.talk.filebrowser.models.properties.OCSize +import com.nextcloud.talk.dagger.modules.RestModule +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.jobs.ShareOperationWorker +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.FileUtils +import com.nextcloud.talk.utils.Mimetype +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Response +import java.io.File +import java.io.IOException +import java.io.RandomAccessFile +import java.nio.channels.FileChannel +import java.util.Locale + +@AutoInjector(NextcloudTalkApplication::class) +class ChunkedFileUploader( + okHttpClient: OkHttpClient, + val currentUser: User, + val roomToken: String, + val metaData: String?, + val listener: OnDataTransferProgressListener +) { + + private var okHttpClientNoRedirects: OkHttpClient? = null + private var remoteChunkUrl: String + private var uploadFolderUri: String = "" + private var isUploadAborted = false + + init { + initHttpClient(okHttpClient, currentUser) + remoteChunkUrl = ApiUtils.getUrlForChunkedUpload(currentUser.baseUrl!!, currentUser.userId!!) + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun upload(localFile: File, mimeType: MediaType?, targetPath: String): Boolean { + try { + uploadFolderUri = remoteChunkUrl + "/" + FileUtils.md5Sum(localFile) + val davResource = DavResource( + okHttpClientNoRedirects!!, + uploadFolderUri.toHttpUrlOrNull()!! + ) + + createFolder(davResource) + + val chunksOnServer: MutableList = getUploadedChunks(davResource, uploadFolderUri) + Log.d(TAG, "chunksOnServer: " + chunksOnServer.size) + + val missingChunks: List = checkMissingChunks(chunksOnServer, localFile.length()) + Log.d(TAG, "missingChunks: " + missingChunks.size) + + for (missingChunk in missingChunks) { + if (isUploadAborted) return false + uploadChunk(localFile, uploadFolderUri, mimeType, missingChunk, missingChunk.length()) + } + + assembleChunks(uploadFolderUri, targetPath) + return true + } catch (e: Exception) { + Log.e(TAG, "Something went wrong in ChunkedFileUploader", e) + return false + } + } + + @Suppress("Detekt.ThrowsCount") + private fun createFolder(davResource: DavResource) { + try { + davResource.mkCol( + xmlBody = null + ) { response: Response -> + if (!response.isSuccessful) { + throw IOException("failed to create folder. response code: " + response.code) + } + } + } catch (e: IOException) { + throw IOException("failed to create folder", e) + } catch (e: HttpException) { + if (e.code == METHOD_NOT_ALLOWED_CODE) { + Log.d(TAG, "Folder most probably already exists, that's okay, just continue..") + } else { + throw IOException("failed to create folder", e) + } + } + } + + @Suppress("Detekt.ComplexMethod") + private fun getUploadedChunks(davResource: DavResource, uploadFolderUri: String): MutableList { + val davResponse = DavResponse() + val memberElements: MutableList = ArrayList() + val rootElement = arrayOfNulls(1) + val remoteFiles: MutableList = ArrayList() + try { + davResource.propfind( + 1 + ) { response: at.bitfire.dav4jvm.Response, hrefRelation: at.bitfire.dav4jvm.Response.HrefRelation? -> + davResponse.setResponse(response) + when (hrefRelation) { + at.bitfire.dav4jvm.Response.HrefRelation.MEMBER -> memberElements.add(response) + at.bitfire.dav4jvm.Response.HrefRelation.SELF -> rootElement[0] = response + at.bitfire.dav4jvm.Response.HrefRelation.OTHER -> {} + else -> {} + } + Unit + } + } catch (e: IOException) { + throw IOException("Error reading remote path", e) + } catch (e: DavException) { + throw IOException("Error reading remote path", e) + } + for (memberElement in memberElements) { + remoteFiles.add( + getModelFromResponse( + memberElement, + memberElement + .href + .toString() + .substring(uploadFolderUri.length) + ) + ) + } + + val chunksOnServer: MutableList = ArrayList() + + for (remoteFile in remoteFiles) { + if (!".file".equals(remoteFile.displayName, ignoreCase = true) && remoteFile.isFile) { + val part: List = remoteFile.displayName!!.split("-") + chunksOnServer.add( + Chunk( + part[0].toLong(), + part[1].toLong() + ) + ) + } + } + return chunksOnServer + } + + private fun checkMissingChunks(chunks: List, length: Long): List { + val missingChunks: MutableList = java.util.ArrayList() + var start: Long = 0 + while (start <= length) { + val nextChunk: Chunk? = findNextFittingChunk(chunks, start) + if (nextChunk == null) { + // create new chunk + val end: Long = if (start + CHUNK_SIZE <= length) { + start + CHUNK_SIZE - 1 + } else { + length + } + missingChunks.add(Chunk(start, end)) + start = end + 1 + } else if (nextChunk.start == start) { + // go to next + start += nextChunk.length() + } else { + // fill the gap + missingChunks.add(Chunk(start, nextChunk.start - 1)) + start = nextChunk.start + } + } + return missingChunks + } + + private fun findNextFittingChunk(chunks: List, start: Long): Chunk? { + for (chunk in chunks) { + if (chunk.start >= start && chunk.start - start <= CHUNK_SIZE) { + return chunk + } + } + return null + } + + private fun uploadChunk( + localFile: File, + uploadFolderUri: String, + mimeType: MediaType?, + chunk: Chunk, + chunkSize: Long + ) { + val startString = java.lang.String.format(Locale.ROOT, "%016d", chunk.start) + val endString = java.lang.String.format(Locale.ROOT, "%016d", chunk.end) + + var raf: RandomAccessFile? = null + var channel: FileChannel? = null + try { + raf = RandomAccessFile(localFile, "r") + channel = raf.channel + + // Log.d(TAG, "chunkSize:$chunkSize") + // Log.d(TAG, "chunk.length():${chunk.length()}") + // Log.d(TAG, "chunk.start:${chunk.start}") + // Log.d(TAG, "chunk.end:${chunk.end}") + + val chunkFromFileRequestBody = ChunkFromFileRequestBody( + localFile, + mimeType, + channel, + chunkSize, + chunk.start, + listener + ) + + val chunkUri = "$uploadFolderUri/$startString-$endString" + + val davResource = DavResource( + okHttpClientNoRedirects!!, + chunkUri.toHttpUrlOrNull()!! + ) + davResource.put( + chunkFromFileRequestBody + ) { response: Response -> + if (!response.isSuccessful) { + throw IOException("Failed to upload chunk. response code: " + response.code) + } + } + } finally { + if (channel != null) { + try { + channel.close() + } catch (e: IOException) { + Log.e(TAG, "Error closing file channel!", e) + } + } + if (raf != null) { + try { + raf.close() + } catch (e: IOException) { + Log.e(TAG, "Error closing file access!", e) + } + } + } + } + + private fun initHttpClient(okHttpClient: OkHttpClient, currentUser: User) { + val okHttpClientBuilder: OkHttpClient.Builder = okHttpClient.newBuilder() + okHttpClientBuilder.followRedirects(false) + okHttpClientBuilder.followSslRedirects(false) + // okHttpClientBuilder.readTimeout(Duration.ofMinutes(30)) // TODO set timeout + okHttpClientBuilder.protocols(listOf(Protocol.HTTP_1_1)) + okHttpClientBuilder.authenticator( + RestModule.HttpAuthenticator( + ApiUtils.getCredentials( + currentUser.username, + currentUser.token + )!!, + "Authorization" + ) + ) + this.okHttpClientNoRedirects = okHttpClientBuilder.build() + } + + private fun assembleChunks(uploadFolderUri: String, targetPath: String) { + val destinationUri: String = ApiUtils.getUrlForFileUpload( + currentUser.baseUrl!!, + currentUser.userId!!, + targetPath + ) + val originUri = "$uploadFolderUri/.file" + + DavResource( + okHttpClientNoRedirects!!, + originUri.toHttpUrlOrNull()!! + ).move( + destinationUri.toHttpUrlOrNull()!!, + true + ) { response: Response -> + if (response.isSuccessful) { + ShareOperationWorker.shareFile( + roomToken, + currentUser, + targetPath, + metaData + ) + } else { + throw IOException("Failed to assemble chunks. response code: " + response.code) + } + } + } + + fun abortUpload(onSuccess: () -> Unit) { + isUploadAborted = true + DavResource( + okHttpClientNoRedirects!!, + uploadFolderUri.toHttpUrlOrNull()!! + ).delete { response: Response -> + when { + response.isSuccessful -> onSuccess() + else -> isUploadAborted = false + } + } + } + + private fun getModelFromResponse(response: at.bitfire.dav4jvm.Response, remotePath: String): RemoteFileBrowserItem { + val remoteFileBrowserItem = RemoteFileBrowserItem() + remoteFileBrowserItem.path = Uri.decode(remotePath) + remoteFileBrowserItem.displayName = Uri.decode(File(remotePath).name) + val properties = response.properties + for (property in properties) { + mapPropertyToBrowserFile(property, remoteFileBrowserItem) + } + if (remoteFileBrowserItem.permissions != null && + remoteFileBrowserItem.permissions!!.contains(READ_PERMISSION) + ) { + remoteFileBrowserItem.isAllowedToReShare = true + } + if (TextUtils.isEmpty(remoteFileBrowserItem.mimeType) && !remoteFileBrowserItem.isFile) { + remoteFileBrowserItem.mimeType = Mimetype.FOLDER + } + + return remoteFileBrowserItem + } + + @Suppress("Detekt.ComplexMethod") + private fun mapPropertyToBrowserFile(property: Property, remoteFileBrowserItem: RemoteFileBrowserItem) { + when (property) { + is OCId -> { + remoteFileBrowserItem.remoteId = property.ocId + } + + is ResourceType -> { + remoteFileBrowserItem.isFile = !property.types.contains(ResourceType.COLLECTION) + } + + is GetLastModified -> { + remoteFileBrowserItem.modifiedTimestamp = property.lastModified + } + + is GetContentType -> { + remoteFileBrowserItem.mimeType = property.type + } + + is OCSize -> { + remoteFileBrowserItem.size = property.ocSize + } + + is NCPreview -> { + remoteFileBrowserItem.hasPreview = property.isNcPreview + } + + is OCFavorite -> { + remoteFileBrowserItem.isFavorite = property.isOcFavorite + } + + is DisplayName -> { + remoteFileBrowserItem.displayName = property.displayName + } + + is NCEncrypted -> { + remoteFileBrowserItem.isEncrypted = property.isNcEncrypted + } + + is NCPermission -> { + remoteFileBrowserItem.permissions = property.ncPermission + } + } + } + + companion object { + private val TAG = ChunkedFileUploader::class.simpleName + private const val READ_PERMISSION = "R" + private const val CHUNK_SIZE: Long = 1024000 + private const val METHOD_NOT_ALLOWED_CODE: Int = 405 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/upload/chunked/OnDataTransferProgressListener.kt b/app/src/main/java/com/nextcloud/talk/upload/chunked/OnDataTransferProgressListener.kt new file mode 100644 index 0000000..652c972 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/upload/chunked/OnDataTransferProgressListener.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.upload.chunked + +interface OnDataTransferProgressListener { + fun onTransferProgress(percentage: Int) +} diff --git a/app/src/main/java/com/nextcloud/talk/upload/normal/FileUploader.kt b/app/src/main/java/com/nextcloud/talk/upload/normal/FileUploader.kt new file mode 100644 index 0000000..89fd9fc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/upload/normal/FileUploader.kt @@ -0,0 +1,180 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022-2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.upload.normal + +import android.content.Context +import android.net.Uri +import android.util.Log +import at.bitfire.dav4jvm.DavResource +import at.bitfire.dav4jvm.exception.HttpException +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.dagger.modules.RestModule +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.jobs.ShareOperationWorker +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.FileUtils +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.RequestBody +import okhttp3.Response +import java.io.File +import java.io.IOException +import java.io.InputStream + +class FileUploader( + okHttpClient: OkHttpClient, + val context: Context, + val currentUser: User, + val roomToken: String, + val ncApi: NcApi, + val file: File +) { + + private var okHttpClientNoRedirects: OkHttpClient? = null + private var okhttpClient: OkHttpClient = okHttpClient + + init { + initHttpClient(okHttpClient, currentUser) + } + + fun upload(sourceFileUri: Uri, fileName: String, remotePath: String, metaData: String?): Observable = + ncApi.uploadFile( + ApiUtils.getCredentials( + currentUser.username, + currentUser.token + ), + ApiUtils.getUrlForFileUpload( + currentUser.baseUrl!!, + currentUser.userId!!, + remotePath + ), + createRequestBody(sourceFileUri) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .flatMap { response -> + if (response.isSuccessful) { + ShareOperationWorker.shareFile( + roomToken, + currentUser, + remotePath, + metaData + ) + FileUtils.copyFileToCache(context, sourceFileUri, fileName) + Observable.just(true) + } else { + if (response.code() == HTTP_CODE_NOT_FOUND || + response.code() == HTTP_CODE_CONFLICT + ) { + createDavResource(sourceFileUri, fileName, remotePath, metaData) + } else { + Observable.just(false) + } + } + } + + private fun createDavResource( + sourceFileUri: Uri, + fileName: String, + remotePath: String, + metaData: String? + ): Observable = + Observable.fromCallable { + val userFileUploadPath = ApiUtils.userFileUploadPath( + currentUser.baseUrl!!, + currentUser.userId!! + ) + val userTalkAttachmentsUploadPath = ApiUtils.userTalkAttachmentsUploadPath( + currentUser.baseUrl!!, + currentUser.userId!! + ) + + var davResource = DavResource( + okHttpClientNoRedirects!!, + userFileUploadPath.toHttpUrlOrNull()!! + ) + createFolder(davResource) + initHttpClient(okHttpClient = okhttpClient, currentUser) + davResource = DavResource( + okHttpClientNoRedirects!!, + userTalkAttachmentsUploadPath.toHttpUrlOrNull()!! + ) + createFolder(davResource) + true + } + .subscribeOn(Schedulers.io()) + .flatMap { upload(sourceFileUri, fileName, remotePath, metaData) } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun createRequestBody(sourceFileUri: Uri): RequestBody? { + var requestBody: RequestBody? = null + try { + val input: InputStream = context.contentResolver.openInputStream(sourceFileUri)!! + input.use { + val buf = ByteArray(input.available()) + while (it.read(buf) != -1) { + requestBody = RequestBody.create("application/octet-stream".toMediaTypeOrNull(), buf) + } + } + } catch (e: Exception) { + Log.e(TAG, "failed to create RequestBody for $sourceFileUri", e) + } + return requestBody + } + + private fun initHttpClient(okHttpClient: OkHttpClient, currentUser: User) { + val okHttpClientBuilder: OkHttpClient.Builder = okHttpClient.newBuilder() + okHttpClientBuilder.followRedirects(false) + okHttpClientBuilder.followSslRedirects(false) + okHttpClientBuilder.protocols(listOf(Protocol.HTTP_1_1)) + okHttpClientBuilder.authenticator( + RestModule.HttpAuthenticator( + ApiUtils.getCredentials( + currentUser.username, + currentUser.token + )!!, + "Authorization" + ) + ) + this.okHttpClientNoRedirects = okHttpClientBuilder.build() + } + + @Suppress("Detekt.ThrowsCount") + private fun createFolder(davResource: DavResource) { + try { + davResource.mkCol( + xmlBody = null + ) { response: Response -> + + if (!response.isSuccessful) { + throw IOException("failed to create folder. response code: " + response.code) + } + } + } catch (e: IOException) { + throw IOException("failed to create folder", e) + } catch (e: HttpException) { + if (e.code == METHOD_NOT_ALLOWED_CODE) { + Log.d(TAG, "Folder most probably already exists, that's okay, just continue..") + } else { + throw IOException("failed to create folder", e) + } + } + } + + companion object { + private val TAG = FileUploader::class.simpleName + private const val METHOD_NOT_ALLOWED_CODE: Int = 405 + private const val HTTP_CODE_NOT_FOUND: Int = 404 + private const val HTTP_CODE_CONFLICT: Int = 409 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt new file mode 100644 index 0000000..326945a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt @@ -0,0 +1,236 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.users + +import android.text.TextUtils +import android.util.Log +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.data.user.UsersRepository +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.ExternalSignalingServer +import com.nextcloud.talk.models.json.capabilities.Capabilities +import com.nextcloud.talk.models.json.capabilities.ServerVersion +import com.nextcloud.talk.models.json.push.PushConfigurationState +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single + +@Suppress("TooManyFunctions") +class UserManager internal constructor(private val userRepository: UsersRepository) { + val users: Single> + get() = userRepository.getUsers() + + val usersScheduledForDeletion: Single> + get() = userRepository.getUsersScheduledForDeletion() + + val currentUser: Maybe + get() { + return userRepository.getActiveUser() + .switchIfEmpty(Maybe.defer { getAnyUserAndSetAsActive() }) + } + + val currentUserObservable: Observable + get() { + return userRepository.getActiveUserObservable() + } + + fun deleteUser(internalId: Long): Int = + userRepository.deleteUser(userRepository.getUserWithId(internalId).blockingGet()) + + fun getUserWithId(id: Long): Maybe = userRepository.getUserWithId(id) + + fun checkIfUserIsScheduledForDeletion(username: String, server: String): Single = + userRepository + .getUserWithUsernameAndServer(username, server) + .map { it.scheduledForDeletion } + .switchIfEmpty(Single.just(false)) + + fun getUserWithInternalId(id: Long): Maybe = userRepository.getUserWithIdNotScheduledForDeletion(id) + + fun checkIfUserExists(username: String, server: String): Single = + userRepository + .getUserWithUsernameAndServer(username, server) + .map { true } + .switchIfEmpty(Single.just(false)) + + /** + * Don't ask + * + * @return `true` if the user was updated **AND** there is another user to set as active, `false` otherwise + */ + fun scheduleUserForDeletionWithId(id: Long): Single = + userRepository.getUserWithId(id) + .map { user -> + user.scheduledForDeletion = true + user.current = false + userRepository.updateUser(user) + } + .flatMap { getAnyUserAndSetAsActive() } + .map { true } + .switchIfEmpty(Single.just(false)) + + private fun getAnyUserAndSetAsActive(): Maybe { + val results = userRepository.getUsersNotScheduledForDeletion() + + return results + .flatMapMaybe { + if (it.isNotEmpty()) { + val user = it.first() + if (setUserAsActive(user).blockingGet()) { + userRepository.getActiveUser() + } else { + Maybe.empty() + } + } else { + Maybe.empty() + } + } + } + + fun updateExternalSignalingServer(id: Long, externalSignalingServer: ExternalSignalingServer): Single = + userRepository.getUserWithId(id).map { user -> + user.externalSignalingServer = externalSignalingServer + userRepository.updateUser(user) + }.toSingle() + + fun updateOrCreateUser(user: User): Single = + Single.fromCallable { + when (user.id) { + null -> userRepository.insertUser(user).toInt() + else -> userRepository.updateUser(user) + } + } + + fun saveUser(user: User): Single = + Single.fromCallable { + userRepository.updateUser(user) + } + + fun setUserAsActive(user: User): Single { + Log.d(TAG, "setUserAsActive:" + user.id!!) + return userRepository.setUserAsActiveWithId(user.id!!) + } + + fun storeProfile(username: String?, userAttributes: UserAttributes): Maybe = + findUser(userAttributes) + .map { user: User? -> + when (user) { + null -> createUser( + username, + userAttributes + ) + else -> { + user.token = userAttributes.token + user.baseUrl = userAttributes.serverUrl + user.current = userAttributes.currentUser + user.userId = userAttributes.userId + user.token = userAttributes.token + user.displayName = userAttributes.displayName + user.clientCertificate = userAttributes.certificateAlias + + updateUserData( + user, + userAttributes + ) + + user + } + } + } + .switchIfEmpty(Maybe.just(createUser(username, userAttributes))) + .map { user -> + userRepository.insertUser(user) + } + .flatMap { id -> + userRepository.getUserWithId(id) + } + + private fun findUser(userAttributes: UserAttributes): Maybe = + if (userAttributes.id != null) { + userRepository.getUserWithId(userAttributes.id) + } else { + Maybe.empty() + } + + private fun updateUserData(user: User, userAttributes: UserAttributes) { + user.userId = userAttributes.userId + user.token = userAttributes.token + user.displayName = userAttributes.displayName + if (userAttributes.pushConfigurationState != null) { + user.pushConfigurationState = LoganSquare + .parse(userAttributes.pushConfigurationState, PushConfigurationState::class.java) + } + if (userAttributes.capabilities != null) { + user.capabilities = LoganSquare + .parse(userAttributes.capabilities, Capabilities::class.java) + } + if (userAttributes.serverVersion != null) { + user.serverVersion = LoganSquare + .parse(userAttributes.serverVersion, ServerVersion::class.java) + } + user.clientCertificate = userAttributes.certificateAlias + if (userAttributes.externalSignalingServer != null) { + user.externalSignalingServer = LoganSquare + .parse(userAttributes.externalSignalingServer, ExternalSignalingServer::class.java) + } + user.current = userAttributes.currentUser == true + } + + private fun createUser(username: String?, userAttributes: UserAttributes): User { + val user = User() + user.baseUrl = userAttributes.serverUrl + user.username = username + user.token = userAttributes.token + if (!TextUtils.isEmpty(userAttributes.displayName)) { + user.displayName = userAttributes.displayName + } + if (userAttributes.pushConfigurationState != null) { + user.pushConfigurationState = LoganSquare + .parse(userAttributes.pushConfigurationState, PushConfigurationState::class.java) + } + if (!TextUtils.isEmpty(userAttributes.userId)) { + user.userId = userAttributes.userId + } + if (!TextUtils.isEmpty(userAttributes.capabilities)) { + user.capabilities = LoganSquare.parse(userAttributes.capabilities, Capabilities::class.java) + } + if (!TextUtils.isEmpty(userAttributes.serverVersion)) { + user.serverVersion = LoganSquare.parse(userAttributes.serverVersion, ServerVersion::class.java) + } + if (!TextUtils.isEmpty(userAttributes.certificateAlias)) { + user.clientCertificate = userAttributes.certificateAlias + } + if (!TextUtils.isEmpty(userAttributes.externalSignalingServer)) { + user.externalSignalingServer = LoganSquare + .parse(userAttributes.externalSignalingServer, ExternalSignalingServer::class.java) + } + user.current = userAttributes.currentUser == true + return user + } + + fun updatePushState(id: Long, state: PushConfigurationState): Single = + userRepository.updatePushState(id, state) + + companion object { + const val TAG = "UserManager" + } + + data class UserAttributes( + val id: Long?, + val serverUrl: String?, + val currentUser: Boolean, + val userId: String?, + val token: String?, + val displayName: String?, + val pushConfigurationState: String?, + val capabilities: String?, + val serverVersion: String?, + val certificateAlias: String?, + val externalSignalingServer: String? + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt new file mode 100644 index 0000000..b474446 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt @@ -0,0 +1,148 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 David Luhmer + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Parts related to account import were either copied from or inspired by the great work done by David Luhmer at: + * https://github.com/nextcloud/ownCloud-Account-Importer + */ +package com.nextcloud.talk.utils + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.ImportAccount +import java.util.Arrays + +object AccountUtils { + + private const val TAG = "AccountUtils" + private const val MIN_SUPPORTED_FILES_APP_VERSION = 30060151 + + fun findAvailableAccountsOnDevice(users: List): List { + val context = NextcloudTalkApplication.sharedApplication!!.applicationContext + val accMgr = AccountManager.get(context) + val accounts = accMgr.getAccountsByType(context.getString(R.string.nc_import_account_type)) + + val accountsAvailable = ArrayList() + var accountFound: Boolean + for (account in accounts) { + accountFound = false + + for (user in users) { + if (matchAccounts(getInformationFromAccount(account), user)) { + accountFound = true + break + } + } + + if (!accountFound) { + accountsAvailable.add(account) + } + } + + return accountsAvailable + } + + private fun matchAccounts(importAccount: ImportAccount, user: User): Boolean { + var accountFound = false + if (importAccount.token != null) { + if (UriUtils.hasHttpProtocolPrefixed(importAccount.baseUrl!!)) { + if ( + user.username == importAccount.username && + user.baseUrl == importAccount.baseUrl + ) { + accountFound = true + } + } else { + if (user.username == importAccount.username && + ( + user.baseUrl == "http://" + importAccount.baseUrl || + user.baseUrl == "https://" + importAccount.baseUrl + ) + ) { + accountFound = true + } + } + } else { + accountFound = true + } + + return accountFound + } + + fun getAppNameBasedOnPackage(packageName: String): String { + val context = NextcloudTalkApplication.sharedApplication!!.applicationContext + val packageManager = context.packageManager + var appName = "" + try { + appName = packageManager.getApplicationLabel( + packageManager.getApplicationInfo( + packageName, + PackageManager.GET_META_DATA + ) + ) as String + } catch (e: PackageManager.NameNotFoundException) { + Log.e(TAG, "Failed to get app name based on package") + } + + return appName + } + + fun canWeOpenFilesApp(context: Context, accountName: String): Boolean { + val pm = context.packageManager + try { + val packageInfo = pm.getPackageInfo(context.getString(R.string.nc_import_accounts_from), 0) + if (packageInfo.versionCode >= MIN_SUPPORTED_FILES_APP_VERSION) { + val ownSignatures = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures + val filesAppSignatures = pm.getPackageInfo( + context.getString(R.string.nc_import_accounts_from), + PackageManager.GET_SIGNATURES + ).signatures + + return if (Arrays.equals(ownSignatures, filesAppSignatures)) { + val accMgr = AccountManager.get(context) + val accounts = accMgr.getAccountsByType(context.getString(R.string.nc_import_account_type)) + accounts.any { it.name == accountName } + } else { + true + } + } + } catch (appNotFoundException: PackageManager.NameNotFoundException) { + // ignore + } + + return false + } + + @Suppress("Detekt.TooGenericExceptionCaught") + fun getInformationFromAccount(account: Account): ImportAccount { + val lastAtPos = account.name.lastIndexOf("@") + var urlString = account.name.substring(lastAtPos + 1) + val username = account.name.substring(0, lastAtPos) + + val context = NextcloudTalkApplication.sharedApplication!!.applicationContext + val accMgr = AccountManager.get(context) + + var password: String? = null + try { + password = accMgr.getPassword(account) + } catch (exception: Exception) { + Log.e(TAG, "Failed to import account") + } + + if (urlString.endsWith("/")) { + urlString = urlString.substring(0, urlString.length - 1) + } + + return ImportAccount(username, password, urlString) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt new file mode 100644 index 0000000..848316f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt @@ -0,0 +1,542 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.net.Uri +import android.text.TextUtils +import android.util.Log +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.RetrofitBucket +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import okhttp3.Credentials.basic +import java.nio.charset.StandardCharsets + +@Suppress("TooManyFunctions") +object ApiUtils { + private val TAG = ApiUtils::class.java.simpleName + const val API_V1 = 1 + private const val API_V2 = 2 + const val API_V3 = 3 + const val API_V4 = 4 + private const val AVATAR_SIZE_BIG = 512 + private const val AVATAR_SIZE_SMALL = 64 + private const val OCS_API_VERSION = "/ocs/v2.php" + private const val SPREED_API_VERSION = "/apps/spreed/api/v1" + private const val SPREED_API_BASE = "$OCS_API_VERSION/apps/spreed/api/v" + + @JvmStatic + val userAgent = "Mozilla/5.0 (Android) Nextcloud-Talk v" + get() = field + BuildConfig.VERSION_NAME + + @Deprecated( + "This is only supported on API v1-3, in API v4+ please use " + + "{@link ApiUtils#getUrlForAttendees(int, String, String)} instead." + ) + fun getUrlForRemovingParticipantFromConversation(baseUrl: String?, roomToken: String?, isGuest: Boolean): String { + var url = getUrlForParticipants(API_V1, baseUrl, roomToken) + if (isGuest) { + url += "/guests" + } + return url + } + + private fun getRetrofitBucketForContactsSearch(baseUrl: String, searchQuery: String?): RetrofitBucket { + var query = searchQuery + val retrofitBucket = RetrofitBucket() + retrofitBucket.url = "$baseUrl$OCS_API_VERSION/apps/files_sharing/api/v1/sharees" + val queryMap: MutableMap = HashMap() + if (query == null) { + query = "" + } + queryMap["format"] = "json" + queryMap["search"] = query + queryMap["itemType"] = "call" + retrofitBucket.queryMap = queryMap + return retrofitBucket + } + + fun getUrlForFilePreviewWithRemotePath(baseUrl: String, remotePath: String?, px: Int): String = + ( + baseUrl + "/index.php/core/preview.png?file=" + + Uri.encode(remotePath, "UTF-8") + + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1" + ) + + fun getUrlForFilePreviewWithFileId(baseUrl: String, fileId: String, px: Int): String = + ( + baseUrl + "/index.php/core/preview?fileId=" + + fileId + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1" + ) + + fun getSharingUrl(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/apps/files_sharing/api/v1/shares" + + fun getRetrofitBucketForContactsSearchFor14(baseUrl: String, searchQuery: String?): RetrofitBucket { + val retrofitBucket = getRetrofitBucketForContactsSearch(baseUrl, searchQuery) + retrofitBucket.url = "$baseUrl$OCS_API_VERSION/core/autocomplete/get" + retrofitBucket.queryMap?.put("itemId", "new") + return retrofitBucket + } + + @JvmStatic + fun getUrlForCapabilities(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/cloud/capabilities" + + @Throws(NoSupportedApiException::class) + fun getCallApiVersion(capabilities: User, versions: IntArray): Int = + getConversationApiVersion(capabilities, versions) + + @JvmStatic + @Throws(NoSupportedApiException::class) + @Suppress("ReturnCount") + fun getConversationApiVersion(user: User, versions: IntArray): Int { + var hasApiV4 = false + for (version in versions) { + hasApiV4 = hasApiV4 or (version == API_V4) + } + if (!hasApiV4) { + val e = Exception("Api call did not try conversation-v4 api") + Log.d(TAG, e.message, e) + } + for (version in versions) { + if (user.hasSpreedFeatureCapability("conversation-v$version")) { + return version + } + + // Fallback for old API versions + if (version == API_V1 || version == API_V2) { + if (user.hasSpreedFeatureCapability("conversation-v2")) { + return version + } + if (version == API_V1 && + user.hasSpreedFeatureCapability("mention-flag") && + !user.hasSpreedFeatureCapability("conversation-v4") + ) { + return version + } + } + } + throw NoSupportedApiException() + } + + @JvmStatic + @Throws(NoSupportedApiException::class) + @Suppress("ReturnCount") + fun getSignalingApiVersion(user: User, versions: IntArray): Int { + val spreedCapabilities = user.capabilities!!.spreedCapability + for (version in versions) { + if (spreedCapabilities != null) { + if (spreedCapabilities.features!!.contains("signaling-v$version")) { + return version + } + if (version == API_V2 && + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SIP_SUPPORT) && + !hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SIGNALING_V3) + ) { + return version + } + if (version == API_V1 && + !hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SIGNALING_V3) + ) { + // Has no capability, we just assume it is always there when there is no v3 or later + return version + } + } + } + throw NoSupportedApiException() + } + + @JvmStatic + @Throws(NoSupportedApiException::class) + fun getChatApiVersion(spreedCapabilities: SpreedCapability, versions: IntArray): Int { + for (version in versions) { + if (version == API_V1 && hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.CHAT_V2)) { + // Do not question that chat-v2 capability shows the availability of api/v1/ endpoint *see no evil* + return version + } + } + throw NoSupportedApiException() + } + + private fun getUrlForApi(version: Int, baseUrl: String?): String = baseUrl + SPREED_API_BASE + version + + fun getUrlForRooms(version: Int, baseUrl: String?): String = getUrlForApi(version, baseUrl) + "/room" + + fun getUrlForNoteToSelf(version: Int, baseUrl: String?): String = + getUrlForApi(version, baseUrl) + "/room/note-to-self" + + @JvmStatic + fun getUrlForRoom(version: Int, baseUrl: String?, token: String?): String = + getUrlForRooms(version, baseUrl) + "/" + token + + fun getUrlForAttendees(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/attendees" + + fun getUrlForParticipants(version: Int, baseUrl: String?, token: String?): String { + if (token.isNullOrEmpty()) { + Log.e(TAG, "token was null or empty") + } + return getUrlForRoom(version, baseUrl, token) + "/participants" + } + + fun getUrlForParticipantsActive(version: Int, baseUrl: String?, token: String?): String = + getUrlForParticipants(version, baseUrl, token) + "/active" + + fun getUrlForImportantConversation(baseUrl: String, roomToken: String): String = + "$baseUrl$OCS_API_VERSION/apps/spreed/api/v4/room/$roomToken/important" + + @JvmStatic + fun getUrlForParticipantsSelf(version: Int, baseUrl: String?, token: String?): String = + getUrlForParticipants(version, baseUrl, token) + "/self" + + fun getUrlForParticipantsResendInvitations(version: Int, baseUrl: String?, token: String?): String = + getUrlForParticipants(version, baseUrl, token) + "/resend-invitations" + + fun getUrlForRoomFavorite(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/favorite" + + fun getUrlForRoomModerators(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/moderators" + + @JvmStatic + fun getUrlForRoomNotificationLevel(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/notify" + + fun getUrlForRoomPublic(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/public" + + fun getUrlForRoomPassword(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/password" + + fun getUrlForConversationReadOnly(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/read-only" + + fun getUrlForRoomWebinaryLobby(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/webinar/lobby" + + @JvmStatic + fun getUrlForRoomNotificationCalls(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/notify-calls" + + fun getUrlForCall(version: Int, baseUrl: String?, token: String): String = + getUrlForApi(version, baseUrl) + "/call/" + token + + fun getUrlForChat(version: Int, baseUrl: String?, token: String): String = + getUrlForApi(version, baseUrl) + "/chat/" + token + + @JvmStatic + fun getUrlForMentionSuggestions(version: Int, baseUrl: String?, token: String): String = + getUrlForChat(version, baseUrl, token) + "/mentions" + + fun getUrlForChatMessage(version: Int, baseUrl: String?, token: String, messageId: String): String = + getUrlForChat(version, baseUrl, token) + "/" + messageId + + fun getUrlForChatSharedItems(version: Int, baseUrl: String?, token: String): String = + getUrlForChat(version, baseUrl, token) + "/share" + + fun getUrlForChatSharedItemsOverview(version: Int, baseUrl: String?, token: String): String = + getUrlForChatSharedItems(version, baseUrl, token) + "/overview" + + fun getUrlForSignaling(version: Int, baseUrl: String?): String = getUrlForApi(version, baseUrl) + "/signaling" + + fun getUrlForTestPushNotifications(baseUrl: String): String = + "$baseUrl$OCS_API_VERSION/apps/notifications/api/v3/test/self" + + @JvmStatic + fun getUrlForSignalingBackend(version: Int, baseUrl: String?): String = + getUrlForSignaling(version, baseUrl) + "/backend" + + @JvmStatic + fun getUrlForSignalingSettings(version: Int, baseUrl: String?): String = + getUrlForSignaling(version, baseUrl) + "/settings" + + fun getUrlForSignalingSettings(version: Int, baseUrl: String?, token: String): String = + getUrlForSignaling(version, baseUrl) + "/settings?token=" + token + + fun getUrlForSignaling(version: Int, baseUrl: String?, token: String): String = + getUrlForSignaling(version, baseUrl) + "/" + token + + fun getUrlForOpenConversations(version: Int, baseUrl: String?): String = + getUrlForApi(version, baseUrl) + "/listed-room" + + @Suppress("LongParameterList") + fun getRetrofitBucketForCreateRoom( + version: Int, + roomType: String, + baseUrl: String? = null, + source: String? = null, + invite: String? = null, + conversationName: String? = null + ): RetrofitBucket { + val retrofitBucket = RetrofitBucket() + retrofitBucket.url = getUrlForRooms(version, baseUrl) + val queryMap: MutableMap = HashMap() + queryMap["roomType"] = roomType + invite?.let { queryMap["invite"] = it } + source?.let { queryMap["source"] = it } + conversationName?.let { queryMap["roomName"] = it } + retrofitBucket.queryMap = queryMap + return retrofitBucket + } + + @JvmStatic + fun getRetrofitBucketForAddParticipant( + version: Int, + baseUrl: String?, + token: String?, + user: String + ): RetrofitBucket { + val retrofitBucket = RetrofitBucket() + retrofitBucket.url = getUrlForParticipants(version, baseUrl, token) + val queryMap: MutableMap = HashMap() + queryMap["newParticipant"] = user + retrofitBucket.queryMap = queryMap + return retrofitBucket + } + + @JvmStatic + fun getRetrofitBucketForAddParticipantWithSource( + version: Int, + baseUrl: String?, + token: String?, + source: String, + id: String + ): RetrofitBucket { + val retrofitBucket = getRetrofitBucketForAddParticipant(version, baseUrl, token, id) + retrofitBucket.queryMap?.put("source", source) + return retrofitBucket + } + + fun getUrlForUserProfile(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/cloud/user" + + fun getUrlForUserData(baseUrl: String, userId: String): String = "$baseUrl$OCS_API_VERSION/cloud/users/$userId" + + fun getUrlForUserSettings(baseUrl: String): String { + // FIXME Introduce API version + return "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/settings/user" + } + + fun getUrlPostfixForStatus(): String = "/status.php" + + @JvmStatic + fun getUrlForAvatar(baseUrl: String?, name: String?, requestBigSize: Boolean): String { + val avatarSize = if (requestBigSize) AVATAR_SIZE_BIG else AVATAR_SIZE_SMALL + return baseUrl + "/index.php/avatar/" + Uri.encode(name) + "/" + avatarSize + } + + @JvmStatic + fun getUrlForAvatarDarkTheme(baseUrl: String?, name: String?, requestBigSize: Boolean): String { + val avatarSize = if (requestBigSize) AVATAR_SIZE_BIG else AVATAR_SIZE_SMALL + return baseUrl + "/index.php/avatar/" + Uri.encode(name) + "/" + avatarSize + "/dark" + } + + @JvmStatic + fun getUrlForFederatedAvatar( + baseUrl: String, + token: String, + cloudId: String, + darkTheme: Int, + requestBigSize: Boolean + ): String { + val avatarSize = if (requestBigSize) AVATAR_SIZE_BIG else AVATAR_SIZE_SMALL + val url = "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/proxy/$token/user-avatar/$avatarSize" + return "$url?cloudId=$cloudId&darkTheme=$darkTheme" + } + + @JvmStatic + fun getUrlForGuestAvatar(baseUrl: String?, name: String?, requestBigSize: Boolean): String { + val avatarSize = if (requestBigSize) AVATAR_SIZE_BIG else AVATAR_SIZE_SMALL + return baseUrl + "/index.php/avatar/guest/" + Uri.encode(name) + "/" + avatarSize + } + + fun getUrlForConversationAvatar(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/avatar" + + fun getUrlForConversationAvatarWithVersion( + version: Int, + baseUrl: String?, + token: String?, + isDark: Boolean, + avatarVersion: String? + ): String { + var isDarkString = "" + if (isDark) { + isDarkString = "/dark" + } + var avatarVersionString = "" + if (avatarVersion != null) { + avatarVersionString = "?avatarVersion=$avatarVersion" + } + return getUrlForRoom(version, baseUrl, token) + "/avatar" + isDarkString + avatarVersionString + } + + @JvmStatic + fun getCredentials(username: String?, token: String?): String? = + if (TextUtils.isEmpty(username) && TextUtils.isEmpty(token)) { + null + } else { + basic(username!!, token!!, StandardCharsets.UTF_8) + } + + @JvmStatic + fun getUrlNextcloudPush(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/apps/notifications/api/v2/push" + + @JvmStatic + fun getUrlPushProxy(): String = + sharedApplication!!.applicationContext.resources.getString(R.string.nc_push_server_url) + "/devices" + + // see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md + fun getUrlForNcNotificationWithId(baseUrl: String, notificationId: String): String = + "$baseUrl$OCS_API_VERSION/apps/notifications/api/v2/notifications/$notificationId" + + fun getUrlForSearchByNumber(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/cloud/users/search/by-phone" + + fun getUrlForUnbindingRoom(baseUrl: String, roomToken: String): String = + "$baseUrl/ocs/v2.php/apps/spreed/api/v4/room/$roomToken/object" + + fun getUrlForFileUpload(baseUrl: String, user: String, remotePath: String): String = + "$baseUrl/remote.php/dav/files/$user$remotePath" + + fun getUrlForChunkedUpload(baseUrl: String, user: String): String = "$baseUrl/remote.php/dav/uploads/$user" + + fun getUrlForFileDownload(baseUrl: String, user: String, remotePath: String): String = + "$baseUrl/remote.php/dav/files/$user/$remotePath" + + fun userFileUploadPath(baseUrl: String, user: String): String = "$baseUrl/remote.php/dav/files/$user" + + fun userTalkAttachmentsUploadPath(baseUrl: String, user: String): String = + "$baseUrl/remote.php/dav/files/$user/Talk" + + fun getUrlForTempAvatar(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/apps/spreed/temp-user-avatar" + + fun getUrlForSensitiveConversation(baseUrl: String, roomToken: String): String = + "$baseUrl$OCS_API_VERSION/apps/spreed/api/v4/room/$roomToken/sensitive" + + fun getUrlForUserFields(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/cloud/user/fields" + + fun getUrlToSendLocation(version: Int, baseUrl: String?, roomToken: String): String = + getUrlForChat(version, baseUrl, roomToken) + "/share" + + fun getUrlForHoverCard(baseUrl: String, userId: String): String = + baseUrl + OCS_API_VERSION + + "/hovercard/v1/" + userId + + fun getUrlForChatReadMarker(version: Int, baseUrl: String?, roomToken: String): String = + getUrlForChat(version, baseUrl, roomToken) + "/read" + + /* + * OCS Status API + */ + @JvmStatic + fun getUrlForStatus(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/user_status" + + @JvmStatic + fun getUrlForBackupStatus(baseUrl: String, userId: String): String = + "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/statuses/_$userId" + + fun getUrlForRevertStatus(baseUrl: String, messageId: String?): String = + "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/user_status/revert/$messageId" + + fun getUrlForSetStatusType(baseUrl: String): String = getUrlForStatus(baseUrl) + "/status" + + fun getUrlForPredefinedStatuses(baseUrl: String): String = + "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/predefined_statuses" + + fun getUrlForStatusMessage(baseUrl: String): String = getUrlForStatus(baseUrl) + "/message" + + fun getUrlForSetCustomStatus(baseUrl: String): String = + "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/user_status/message/custom" + + fun getUrlForSetPredefinedStatus(baseUrl: String): String = + "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/user_status/message/predefined" + + fun getUrlForUserStatuses(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/apps/user_status/api/v1/statuses" + + fun getUrlForMessageReaction(baseUrl: String, roomToken: String, messageId: String): String = + "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/reaction/$roomToken/$messageId" + + fun getUrlForUnifiedSearch(baseUrl: String, providerId: String): String = + "$baseUrl$OCS_API_VERSION/search/providers/$providerId/search" + + fun getUrlForPoll(baseUrl: String, roomToken: String, pollId: String): String = + getUrlForPoll(baseUrl, roomToken) + "/" + pollId + + fun getUrlForPoll(baseUrl: String, roomToken: String): String = + "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/poll/$roomToken" + + @JvmStatic + fun getUrlForMessageExpiration(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/message-expiration" + + fun getUrlForOpenGraph(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/references/resolve" + + fun getUrlForRecording(version: Int, baseUrl: String?, token: String): String = + getUrlForApi(version, baseUrl) + "/recording/" + token + + fun getUrlForRequestAssistance(version: Int, baseUrl: String?, token: String): String = + getUrlForApi(version, baseUrl) + "/breakout-rooms/" + token + "/request-assistance" + + fun getUrlForConversationDescription(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/description" + + fun getUrlForOpeningConversations(version: Int, baseUrl: String?, token: String): String = + getUrlForRoom(version, baseUrl, token) + "/listable" + + fun getUrlForTranslation(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/translation/translate" + + fun getUrlForLanguages(baseUrl: String): String = "$baseUrl$OCS_API_VERSION/translation/languages" + + fun getUrlForReminder(user: User, roomToken: String, messageId: String, version: Int): String { + val url = getUrlForChatMessage(version, user.baseUrl!!, roomToken, messageId) + return "$url/reminder" + } + + fun getUrlForRecordingConsent(version: Int, baseUrl: String?, token: String?): String = + getUrlForRoom(version, baseUrl, token) + "/recording-consent" + + fun getUrlForInvitation(baseUrl: String): String = + baseUrl + OCS_API_VERSION + SPREED_API_VERSION + "/federation/invitation" + + fun getUrlForInvitationAccept(baseUrl: String, id: Int): String = getUrlForInvitation(baseUrl) + "/" + id + + fun getUrlForInvitationReject(baseUrl: String, id: Int): String = getUrlForInvitation(baseUrl) + "/" + id + + @JvmStatic + fun getUrlForRoomCapabilities(version: Int, baseUrl: String?, token: String?): String = + getUrlForRooms(version, baseUrl) + "/" + token + "/capabilities" + + fun getUrlForBans(baseUrl: String, token: String): String = "$baseUrl/ocs/v1.php$SPREED_API_VERSION/ban/$token" + + fun getUrlForUnban(baseUrl: String, token: String, banId: Int): String = "${getUrlForBans(baseUrl, token)}/$banId" + + fun getUrlForArchive(version: Int, baseUrl: String?, token: String?): String = + "${getUrlForRoom(version, baseUrl, token)}/archive" + + fun getUrlForOutOfOffice(baseUrl: String, userId: String): String = + "$baseUrl$OCS_API_VERSION/apps/dav/api/v1/outOfOffice/$userId/now" + + fun getUrlForChatMessageContext(baseUrl: String, token: String, messageId: String): String = + "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/chat/$token/$messageId/context" + + fun getUrlForProfile(baseUrl: String, userId: String): String = "$baseUrl$OCS_API_VERSION/profile/$userId" + + fun getUrlForRecentThreads(version: Int, baseUrl: String?, token: String): String = + getUrlForChat(version, baseUrl, token) + "/threads/recent" + + fun getUrlForSubscribedThreads(version: Int, baseUrl: String?): String = + getUrlForApi(version, baseUrl) + "/chat/subscribed-threads" + + fun getUrlForThread(version: Int, baseUrl: String?, token: String, threadId: Int): String = + getUrlForChat(version, baseUrl, token) + "/threads" + "/$threadId" + + fun getUrlForThreadNotificationLevel(version: Int, baseUrl: String?, token: String, threadId: Int): String = + getUrlForChat(version, baseUrl, token) + "/threads" + "/$threadId" + "/notify" +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/AppCompatActivityExtensions.kt b/app/src/main/java/com/nextcloud/talk/utils/AppCompatActivityExtensions.kt new file mode 100644 index 0000000..9fe321b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/AppCompatActivityExtensions.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.utils + +import android.graphics.Color +import android.os.Build +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity + +/** + * This method is similar to "adjustUIForAPILevel35" in + * AppCompatActivityExtensions.kt in https://github.com/nextcloud/android-common/ + * Only window.addSystemBarPaddings() had to be removed. This could be unified again at some point. + */ +@JvmOverloads +fun AppCompatActivity.adjustUIForAPILevel35( + statusBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT), + // It may make sense to change navigationBarStyle to "SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)" + // For now, it is set to "light" to have a fully transparent navigation bar to align with the XML screens. + // It may be wanted to have a semi transparent navigation bar in the future. Then set it to "auto" and try to + // migrate the XML screens to Compose (having semi transparent navigation bar for XML did not work out. In + // general, supporting both XML and Compose system bar handling is a pain and we will have it easier without XML) + // So in short: migrate all screens to Compose. Then it's easier to decide if navigation bar should be semi + // transparent or not for all screens. + navigationBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT) +) { + val isApiLevel35OrHigher = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) + if (!isApiLevel35OrHigher) { + return + } + enableEdgeToEdge(statusBarStyle, navigationBarStyle) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/AudioUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/AudioUtils.kt new file mode 100644 index 0000000..e5f6809 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/AudioUtils.kt @@ -0,0 +1,231 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.media.AudioFormat +import android.media.MediaCodec +import android.media.MediaCodec.CodecException +import android.media.MediaCodecList +import android.media.MediaExtractor +import android.media.MediaFormat +import android.os.SystemClock +import android.util.Log +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import java.io.File +import java.io.IOException +import java.nio.ByteOrder +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import kotlin.math.abs + +/** + * AudioUtils are for processing raw audio using android's low level APIs, for more information read here + * [MediaCodec documentation](https://developer.android.com/reference/android/media/MediaCodec) + */ +object AudioUtils : DefaultLifecycleObserver { + private val TAG = AudioUtils::class.java.simpleName + private const val VALUE_10 = 10 + private const val TIME_LIMIT = 3000 + private const val DEFAULT_SIZE = 500 + private enum class LifeCycleFlag { + PAUSED, + RESUMED + } + private lateinit var currentLifeCycleFlag: LifeCycleFlag + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + currentLifeCycleFlag = LifeCycleFlag.RESUMED + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + currentLifeCycleFlag = LifeCycleFlag.PAUSED + } + + /** + * Suspension function, returns a FloatArray of size 500, containing the values of an audio file squeezed between + * [0,1) + */ + @Throws(IOException::class) + suspend fun audioFileToFloatArray(file: File): FloatArray { + return suspendCoroutine { + // Used to keep track of the time it took to process the audio file + val startTime = SystemClock.elapsedRealtime() + + // Always a FloatArray of Size 500 + var result: MutableList? = mutableListOf() + + // Setting the file path to the audio file + val path = file.path + val mediaExtractor = MediaExtractor() + mediaExtractor.setDataSource(path) + + // Basically just boilerplate to set up meta data for the audio file + val mediaFormat = mediaExtractor.getTrackFormat(0) + // Frame rate is required for encoders, optional for decoders. So we set it to null here. + mediaFormat.setString(MediaFormat.KEY_FRAME_RATE, null) + mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 0) + + mediaExtractor.release() + + // More Boiler plate to set up the codec + val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS) + val codecName = mediaCodecList.findDecoderForFormat(mediaFormat) + val mediaCodec = MediaCodec.createByCodecName(codecName) + + /** + ************************************ Media Codec ******************************************* + * │ + * INPUT BUFFERS │ OUTPUT BUFFERS + * │ + * ┌────────────────┐ ┌───────┴────────┐ ┌─────────────────┐ + * │ │ Empty Buffer│ │ Filled Buffer│ │ + * │ │ [][][] │ │ [-][-][-] │ │ + * │ │ ◄───────────┤ ├────────────► │ │ + * │ Client │ │ Codec │ │ Client │ + * │ │ │ │ │ │ + * │ ├───────────► │ │ ◄────────────┤ │ + * │ │ [-][-][-] │ │ [][][] │ │ + * └────────────────┘Filled Buffer└───────┬────────┘Empty Buffer └─────────────────┘ + * │ + * Client provides │ Client consumes + * input Data │ output data + * + ******************************************************************************************** + */ + mediaCodec.setCallback(object : MediaCodec.Callback() { + private var extractor: MediaExtractor? = null + val tempList = mutableListOf() + init { + // Setting up the extractor to be guaranteed not null + extractor = MediaExtractor() + try { + extractor!!.setDataSource(path) + extractor!!.selectTrack(0) + } catch (e: IOException) { + e.printStackTrace() + } + } + + override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { + // Boiler plate, Extracts a buffer of encoded audio data to be sent to the codec for processing + val byteBuffer = codec.getInputBuffer(index) + if (byteBuffer != null && extractor != null) { + val sampleSize = extractor!!.readSampleData(byteBuffer, 0) + if (sampleSize > 0) { + val isOver = !extractor!!.advance() + codec.queueInputBuffer( + index, + 0, + sampleSize, + extractor!!.sampleTime, + if (isOver) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0 + ) + } + } + } + + override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) { + // Boiler plate to get the audio data in a usable form + val outputBuffer = codec.getOutputBuffer(index) + val bufferFormat = codec.getOutputFormat(index) + val samples = outputBuffer!!.order(ByteOrder.nativeOrder()).asShortBuffer() + val numChannels = bufferFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) + if (index < 0 || index >= numChannels) { + return + } + val sampleLength = (samples.remaining() / numChannels) + + // Squeezes the value of each sample between [0,1) using y = (x-1)/x + for (i in 0 until sampleLength) { + val x = abs(samples[i * numChannels + index].toInt()) / VALUE_10 + val y = (if (x > 0) ((x - 1) / x.toFloat()) else x.toFloat()) + tempList.add(y) + } + + codec.releaseOutputBuffer(index, false) + + // Cancels the process if it ends, exceeds the time limit, or the activity falls out of view + val currTime = SystemClock.elapsedRealtime() - startTime + if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM > 0 || + currTime > TIME_LIMIT || + currentLifeCycleFlag == LifeCycleFlag.PAUSED + ) { + Log.d( + TAG, + "Processing ended with time: $currTime \n" + + "Is finished: ${info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM > 0} \n" + + "Lifecycle state: $currentLifeCycleFlag" + ) + codec.stop() + codec.release() + extractor!!.release() + extractor = null + result = if (currTime < TIME_LIMIT) { + tempList + } else { + Log.e(TAG, "Error in MediaCodec Callback:\n\tonOutputBufferAvailable: Time limit exceeded") + null + } + } + } + + override fun onError(codec: MediaCodec, e: CodecException) { + Log.e(TAG, "Error in MediaCodec Callback: \n$e") + codec.stop() + codec.release() + extractor!!.release() + extractor = null + result = null + } + + override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { + // unused atm + } + }) + + // More Boiler plate to start the codec + mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT) + mediaCodec.configure(mediaFormat, null, null, 0) + mediaCodec.start() + + // This runs until the codec finishes, the time limit is exceeded, or an error occurs + // If the time limit is exceed or an error occurs, the result should be null or empty + var currTime = SystemClock.elapsedRealtime() - startTime + while (result != null && + result!!.size <= 0 && + currTime < TIME_LIMIT // Guarantees Execution stops after 3 seconds + ) { + currTime = SystemClock.elapsedRealtime() - startTime + continue + } + + if (result != null && result!!.size > DEFAULT_SIZE) { + it.resume(shrinkFloatArray(result!!.toFloatArray(), DEFAULT_SIZE)) + } else { + it.resume(FloatArray(DEFAULT_SIZE)) + } + } + } + + fun shrinkFloatArray(data: FloatArray, size: Int): FloatArray { + val result = FloatArray(size) + val scale = data.size / size + var begin = 0 + var end = scale + for (i in 0 until size) { + val arr = data.copyOfRange(begin, end) + result[i] = arr.average().toFloat() + begin += scale + end += scale + } + + return result + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/AuthenticatorService.java b/app/src/main/java/com/nextcloud/talk/utils/AuthenticatorService.java new file mode 100644 index 0000000..388661b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/AuthenticatorService.java @@ -0,0 +1,87 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils; + + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.accounts.NetworkErrorException; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +public class AuthenticatorService extends Service { + + private static Authenticator authenticator = null; + + private static class Authenticator extends AbstractAccountAuthenticator { + public Authenticator(Context context) { + super(context); + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) { + return null; + } + + @Override + public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, Account account) throws NetworkErrorException { + return super.getAccountRemovalAllowed(response, account); + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { + return null; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public String getAuthTokenLabel(String authTokenType) { + return null; + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) { + return null; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) { + return null; + } + } + + protected Authenticator getAuthenticator() { + if (authenticator == null) { + authenticator = new Authenticator(this); + } + return authenticator; + } + + + @Override + public IBinder onBind(Intent intent) { + if (AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) { + return getAuthenticator().getIBinder(); + } else { + return null; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt b/app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt new file mode 100644 index 0000000..17db322 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/BitmapShrinker.kt @@ -0,0 +1,86 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.util.Log +import androidx.exifinterface.media.ExifInterface +import java.io.IOException + +object BitmapShrinker { + + private val TAG = "BitmapShrinker" + private const val DEGREES_90 = 90f + private const val DEGREES_180 = 180f + private const val DEGREES_270 = 270f + + @JvmStatic + fun shrinkBitmap(path: String, reqWidth: Int, reqHeight: Int): Bitmap { + val bitmap = decodeBitmap(path, reqWidth, reqHeight) + return rotateBitmap(path, bitmap) + } + + // solution inspired by https://developer.android.com/topic/performance/graphics/load-bitmap + private fun decodeBitmap(path: String, requestedWidth: Int, requestedHeight: Int): Bitmap = + BitmapFactory.Options().run { + inJustDecodeBounds = true + BitmapFactory.decodeFile(path, this) + inSampleSize = getInSampleSize(this, requestedWidth, requestedHeight) + inJustDecodeBounds = false + BitmapFactory.decodeFile(path, this) + } + + // solution inspired by https://developer.android.com/topic/performance/graphics/load-bitmap + private fun getInSampleSize(options: BitmapFactory.Options, requestedWidth: Int, requestedHeight: Int): Int { + val (height: Int, width: Int) = options.run { outHeight to outWidth } + var inSampleSize = 1 + if (height > requestedHeight || width > requestedWidth) { + val halfHeight: Int = height / 2 + val halfWidth: Int = width / 2 + // "||" was used instead of "&&". Otherwise it would still crash for wide panorama photos. + while (halfHeight / inSampleSize >= requestedHeight || halfWidth / inSampleSize >= requestedWidth) { + inSampleSize *= 2 + } + } + return inSampleSize + } + + // solution inspired by https://stackoverflow.com/a/15341203 + private fun rotateBitmap(path: String, bitmap: Bitmap): Bitmap { + try { + val exif = ExifInterface(path) + val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1) + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> { + matrix.postRotate(DEGREES_90) + } + ExifInterface.ORIENTATION_ROTATE_180 -> { + matrix.postRotate(DEGREES_180) + } + ExifInterface.ORIENTATION_ROTATE_270 -> { + matrix.postRotate(DEGREES_270) + } + } + val rotatedBitmap = Bitmap.createBitmap( + bitmap, + 0, + 0, + bitmap.getWidth(), + bitmap.getHeight(), + matrix, + true + ) + return rotatedBitmap + } catch (e: IOException) { + Log.e(TAG, "error while rotating image", e) + } + return bitmap + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/BrandingUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/BrandingUtils.kt new file mode 100644 index 0000000..400fc28 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/BrandingUtils.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.Context + +object BrandingUtils { + private const val ORIGINAL_NEXTCLOUD_TALK_APPLICATION_ID = "com.nextcloud.talk2" + + fun isOriginalNextcloudClient(context: Context): Boolean = + context.packageName.equals(ORIGINAL_NEXTCLOUD_TALK_APPLICATION_ID) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt new file mode 100644 index 0000000..75af1dd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt @@ -0,0 +1,334 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023-2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.util.Log +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.json.capabilities.SpreedCapability + +enum class SpreedFeatures(val value: String) { + RECORDING_V1("recording-v1"), + REACTIONS("reactions"), + RAISE_HAND("raise-hand"), + DIRECT_MENTION_FLAG("direct-mention-flag"), + CONVERSATION_CALL_FLAGS("conversation-call-flags"), + SILENT_SEND("silent-send"), + MENTION_FLAG("mention-flag"), + DELETE_MESSAGES("delete-messages"), + READ_ONLY_ROOMS("read-only-rooms"), + RICH_OBJECT_LIST_MEDIA("rich-object-list-media"), + SILENT_CALL("silent-call"), + MESSAGE_EXPIRATION("message-expiration"), + WEBINARY_LOBBY("webinary-lobby"), + VOICE_MESSAGE_SHARING("voice-message-sharing"), + INVITE_GROUPS_AND_MAILS("invite-groups-and-mails"), + CIRCLES_SUPPORT("circles-support"), + LAST_ROOM_ACTIVITY("last-room-activity"), + NOTIFICATION_LEVELS("notification-levels"), + CLEAR_HISTORY("clear-history"), + AVATAR("avatar"), + LISTABLE_ROOMS("listable-rooms"), + LOCKED_ONE_TO_ONE_ROOMS("locked-one-to-one-rooms"), + TEMP_USER_AVATAR_API("temp-user-avatar-api"), + PHONEBOOK_SEARCH("phonebook-search"), + GEO_LOCATION_SHARING("geo-location-sharing"), + TALK_POLLS("talk-polls"), + FAVORITES("favorites"), + CHAT_READ_MARKER("chat-read-marker"), + CHAT_UNREAD("chat-unread"), + EDIT_MESSAGES("edit-messages"), + REMIND_ME_LATER("remind-me-later"), + CHAT_V2("chat-v2"), + SIP_SUPPORT("sip-support"), + SIGNALING_V3("signaling-v3"), + ROOM_DESCRIPTION("room-description"), + UNIFIED_SEARCH("unified-search"), + LOCKED_ONE_TO_ONE("locked-one-to-one-rooms"), + CHAT_PERMISSION("chat-permission"), + CONVERSATION_PERMISSION("conversation-permissions"), + FEDERATION_V1("federation-v1"), + DELETE_MESSAGES_UNLIMITED("delete-messages-unlimited"), + BAN_V1("ban-v1"), + EDIT_MESSAGES_NOTE_TO_SELF("edit-messages-note-to-self"), + ARCHIVE_CONVERSATIONS("archived-conversations-v2"), + CONVERSATION_CREATION_ALL("conversation-creation-all"), + UNBIND_CONVERSATION("unbind-conversation"), + SENSITIVE_CONVERSATIONS("sensitive-conversations"), + IMPORTANT_CONVERSATIONS("important-conversations"), + THREADS("threads") +} + +@Suppress("TooManyFunctions") +object CapabilitiesUtil { + + //region Version checks + fun isServerEOL(serverVersion: Int?): Boolean { + if (serverVersion == null) { + Log.w(TAG, "serverVersion is unknown. It is assumed that it is up to date") + return false + } + return (serverVersion < SERVER_VERSION_MIN_SUPPORTED) + } + + fun isServerAlmostEOL(serverVersion: Int?): Boolean { + if (serverVersion == null) { + Log.w(TAG, "serverVersion is unknown. It is assumed that it is up to date") + return false + } + return (serverVersion < SERVER_VERSION_SUPPORT_WARNING) + } + + // endregion + + //region CoreCapabilities + + @JvmStatic + fun isLinkPreviewAvailable(user: User): Boolean = + user.capabilities?.coreCapability?.referenceApi != null && + user.capabilities?.coreCapability?.referenceApi == "true" + + fun canGeneratePrettyURL(user: User): Boolean = user.capabilities?.coreCapability?.modRewriteWorking == true + + // endregion + + //region SpreedCapabilities + + @JvmStatic + fun hasSpreedFeatureCapability(spreedCapabilities: SpreedCapability, spreedFeatures: SpreedFeatures): Boolean { + if (spreedCapabilities.features != null) { + return spreedCapabilities.features!!.contains(spreedFeatures.value) + } + return false + } + + fun isSharedItemsAvailable(spreedCapabilities: SpreedCapability): Boolean = + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RICH_OBJECT_LIST_MEDIA) + + fun getMessageMaxLength(spreedCapabilities: SpreedCapability): Int { + if (spreedCapabilities.config?.containsKey("chat") == true) { + val chatConfigHashMap = spreedCapabilities.config!!["chat"] + if (chatConfigHashMap?.containsKey("max-length") == true) { + val chatSize = (chatConfigHashMap["max-length"]!!.toString()).toInt() + return if (chatSize > 0) { + chatSize + } else { + DEFAULT_CHAT_SIZE + } + } + } + + return DEFAULT_CHAT_SIZE + } + + fun conversationDescriptionLength(spreedCapabilities: SpreedCapability): Int { + if (spreedCapabilities.config?.containsKey("conversations") == true) { + val map: Map? = spreedCapabilities.config!!["conversations"] + if (map != null && map.containsKey("description-length")) { + return (map["description-length"].toString().toInt()) + } + } + return CONVERSATION_DESCRIPTION_LENGTH_FOR_OLD_SERVER + } + + fun isReadStatusAvailable(spreedCapabilities: SpreedCapability): Boolean { + if (spreedCapabilities.config?.containsKey("chat") == true) { + val map: Map? = spreedCapabilities.config!!["chat"] + return map != null && map.containsKey("read-privacy") + } + return false + } + + fun retentionOfEventRooms(spreedCapabilities: SpreedCapability): Int { + if (spreedCapabilities.config?.containsKey("conversations") == true) { + val map = spreedCapabilities.config!!["conversations"] + if (map?.containsKey("retention-event") == true) { + return map["retention-event"].toString().toInt() + } + } + return 0 + } + + fun retentionOfSIPRoom(spreedCapabilities: SpreedCapability): Int { + if (spreedCapabilities.config?.containsKey("conversations") == true) { + val map = spreedCapabilities.config!!["conversations"] + if (map?.containsKey("retention-phone") == true) { + return map["retention-phone"].toString().toInt() + } + } + return 0 + } + + fun retentionOfInstantMeetingRoom(spreedCapabilities: SpreedCapability): Int { + if (spreedCapabilities.config?.containsKey("conversations") == true) { + val map = spreedCapabilities.config!!["conversations"] + if (map?.containsKey("retention-instant-meetings") == true) { + return map["retention-instant-meetings"].toString().toInt() + } + } + return 0 + } + + @JvmStatic + fun isCallRecordingAvailable(spreedCapabilities: SpreedCapability): Boolean { + if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RECORDING_V1) && + spreedCapabilities.config?.containsKey("call") == true + ) { + val map: Map? = spreedCapabilities.config!!["call"] + if (map != null && map.containsKey("recording")) { + return (map["recording"].toString()).toBoolean() + } + } + return false + } + + @JvmStatic + fun getAttachmentFolder(spreedCapabilities: SpreedCapability): String { + if (spreedCapabilities.config?.containsKey("attachments") == true) { + val map = spreedCapabilities.config!!["attachments"] + if (map?.containsKey("folder") == true) { + return map["folder"].toString() + } + } + return "/Talk" + } + + fun isConversationDescriptionEndpointAvailable(spreedCapabilities: SpreedCapability): Boolean = + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.ROOM_DESCRIPTION) + + fun isUnifiedSearchAvailable(spreedCapabilities: SpreedCapability): Boolean = + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.UNIFIED_SEARCH) + + fun isAbleToCall(spreedCapabilities: SpreedCapability): Boolean = + if ( + spreedCapabilities.config?.containsKey("call") == true && + spreedCapabilities.config!!["call"] != null && + spreedCapabilities.config!!["call"]!!.containsKey("enabled") + ) { + java.lang.Boolean.parseBoolean(spreedCapabilities.config!!["call"]!!["enabled"].toString()) + } else { + // older nextcloud versions without the capability can't disable the calls + true + } + + fun isCallReactionsSupported(user: User?): Boolean { + if (user?.capabilities != null) { + val capabilities = user.capabilities + return capabilities?.spreedCapability?.config?.containsKey("call") == true && + capabilities.spreedCapability!!.config!!["call"] != null && + capabilities.spreedCapability!!.config!!["call"]!!.containsKey("supported-reactions") + } + return false + } + + fun isTranslationsSupported(spreedCapabilities: SpreedCapability): Boolean = + spreedCapabilities.config?.containsKey("chat") == true && + spreedCapabilities.config!!["chat"] != null && + spreedCapabilities.config!!["chat"]!!.containsKey("has-translation-providers") && + spreedCapabilities.config!!["chat"]!!["has-translation-providers"] == true + + fun getRecordingConsentType(spreedCapabilities: SpreedCapability): Int { + if ( + spreedCapabilities.config?.containsKey("call") == true && + spreedCapabilities.config!!["call"] != null && + spreedCapabilities.config!!["call"]!!.containsKey("recording-consent") + ) { + return when ( + spreedCapabilities.config!!["call"]!!["recording-consent"].toString() + .toInt() + ) { + 1 -> RECORDING_CONSENT_REQUIRED + 2 -> RECORDING_CONSENT_DEPEND_ON_CONVERSATION + else -> RECORDING_CONSENT_NOT_REQUIRED + } + } + return RECORDING_CONSENT_NOT_REQUIRED + } + + fun isBanningAvailable(spreedCapabilities: SpreedCapability): Boolean = + hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.BAN_V1) + + // endregion + + //region SpreedCapabilities that can't be used with federation as the settings for them are global + + fun isReadStatusPrivate(user: User): Boolean { + if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) { + val map = user.capabilities!!.spreedCapability!!.config!!["chat"] + if (map?.containsKey("read-privacy") == true) { + return (map["read-privacy"]!!.toString()).toInt() == 1 + } + } + return false + } + + fun isTypingStatusAvailable(user: User): Boolean { + if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) { + val map = user.capabilities!!.spreedCapability!!.config!!["chat"] + return map != null && map.containsKey("typing-privacy") + } + return false + } + + fun isTypingStatusPrivate(user: User): Boolean { + if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) { + val map = user.capabilities!!.spreedCapability!!.config!!["chat"] + if (map?.containsKey("typing-privacy") == true) { + return (map["typing-privacy"]!!.toString()).toInt() == 1 + } + } + return false + } + + fun isFederationAvailable(user: User): Boolean = + hasSpreedFeatureCapability(user.capabilities!!.spreedCapability!!, SpreedFeatures.FEDERATION_V1) && + user.capabilities!!.spreedCapability!!.config?.containsKey("federation") == true && + user.capabilities!!.spreedCapability!!.config!!["federation"] != null && + user.capabilities!!.spreedCapability!!.config!!["federation"]!!.containsKey("enabled") + + // endregion + + //region ThemingCapabilities + + fun getServerName(user: User?): String? { + if (user?.capabilities?.themingCapability != null) { + return user.capabilities!!.themingCapability!!.name + } + return "" + } + + // endregion + + //region ProvisioningCapabilities + + fun canEditScopes(user: User): Boolean = + user.capabilities?.provisioningCapability?.accountPropertyScopesVersion != null && + user.capabilities!!.provisioningCapability!!.accountPropertyScopesVersion!! > 1 + + // endregion + + //region UserStatusCapabilities + + @JvmStatic + fun isUserStatusAvailable(user: User): Boolean = + user.capabilities?.userStatusCapability?.enabled == true && + user.capabilities?.userStatusCapability?.supportsEmoji == true + + fun isRestoreStatusAvailable(user: User): Boolean = user.capabilities?.userStatusCapability?.restore == true + + // endregion + + private val TAG = CapabilitiesUtil::class.java.simpleName + const val DEFAULT_CHAT_SIZE = 1000 + const val RECORDING_CONSENT_NOT_REQUIRED = 0 + const val RECORDING_CONSENT_REQUIRED = 1 + const val RECORDING_CONSENT_DEPEND_ON_CONVERSATION = 2 + private const val SERVER_VERSION_MIN_SUPPORTED = 17 + private const val SERVER_VERSION_SUPPORT_WARNING = 26 + private const val CONVERSATION_DESCRIPTION_LENGTH_FOR_OLD_SERVER = 500 +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/CharPolicy.java b/app/src/main/java/com/nextcloud/talk/utils/CharPolicy.java new file mode 100644 index 0000000..add1d48 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/CharPolicy.java @@ -0,0 +1,110 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils; + +import android.text.Spannable; +import android.text.Spanned; +import androidx.annotation.Nullable; +import com.otaliastudios.autocomplete.AutocompletePolicy; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CharPolicy implements AutocompletePolicy { + + private final char character; + + public CharPolicy(char character) { + this.character = character; + } + + @Nullable + public static TextSpan getQueryRange(Spannable text) { + QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class); + if (span == null || span.length == 0) { + return null; + } else { + QuerySpan sp = span[0]; + return new TextSpan(text.getSpanStart(sp), text.getSpanEnd(sp)); + } + } + + private TextSpan checkText(Spannable text, int cursorPos) { + if (text.length() == 0) { + return null; + } + + Pattern pattern = Pattern.compile("@+\\S*", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); + Matcher matcher = pattern.matcher(text); + + while (matcher.find()) { + if (cursorPos >= matcher.start() && cursorPos <= matcher.end() && + text.subSequence(matcher.start(), matcher.end()).charAt(0) == character) { + return new TextSpan(matcher.start(), matcher.end()); + } + } + + return null; + } + + public static class TextSpan { + int start; + int end; + + public TextSpan(int start, int end) { + this.start = start; + this.end = end; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + } + + @Override + public boolean shouldShowPopup(Spannable text, int cursorPos) { + TextSpan show = checkText(text, cursorPos); + if (show != null) { + text.setSpan(new QuerySpan(), show.getStart(), show.getEnd(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + return true; + } + return false; + } + + @Override + public boolean shouldDismissPopup(Spannable text, int cursorPos) { + return checkText(text, cursorPos) == null; + } + + @Override + public CharSequence getQuery(Spannable text) { + QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class); + if (span == null || span.length == 0) { + // Should never happen. + return ""; + } + QuerySpan sp = span[0]; + return text.subSequence(text.getSpanStart(sp), text.getSpanEnd(sp)); + } + + @Override + public void onDismiss(Spannable text) { + // Remove any span added by shouldShow. Should be useless, but anyway. + QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class); + for (QuerySpan s : span) { + text.removeSpan(s); + } + } + + private static class QuerySpan { + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/utils/ChatMessageUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ChatMessageUtils.kt new file mode 100644 index 0000000..9d271bf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ChatMessageUtils.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.view.View +import android.widget.ImageView +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.extensions.loadBotsAvatar +import com.nextcloud.talk.extensions.loadChangelogBotAvatar +import com.nextcloud.talk.extensions.loadDefaultAvatar +import com.nextcloud.talk.extensions.loadFederatedUserAvatar +import com.nextcloud.talk.extensions.loadFirstLetterAvatar +import com.nextcloud.talk.ui.theme.ViewThemeUtils + +class ChatMessageUtils { + + fun setAvatarOnMessage(view: ImageView, message: ChatMessage, viewThemeUtils: ViewThemeUtils) { + view.visibility = View.VISIBLE + if (message.actorType == "guests" || message.actorType == "emails") { + val actorName = message.actorDisplayName + if (!actorName.isNullOrBlank()) { + view.loadFirstLetterAvatar(actorName) + } else { + view.loadDefaultAvatar(viewThemeUtils) + } + } else if (message.actorType == "bots" && (message.actorId == "changelog" || message.actorId == "sample")) { + view.loadChangelogBotAvatar() + } else if (message.actorType == "bots") { + view.loadBotsAvatar() + } else if (message.actorType == "federated_users") { + view.loadFederatedUserAvatar(message) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt b/app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt new file mode 100644 index 0000000..57bec12 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt @@ -0,0 +1,69 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.utils + +import android.graphics.Color +import java.security.MessageDigest + +// See https://github.com/nextcloud/nextcloud-vue/blob/56b79afae93f4701a0cb933bfeb7b4a2fbd590fb/src/functions/usernameToColor/usernameToColor.js +// and https://github.com/nextcloud/nextcloud-vue/blob/56b79afae93f4701a0cb933bfeb7b4a2fbd590fb/src/utils/GenColors.js + +@Suppress("MagicNumber") +object ColorGenerator { + + private const val STEPS = 6 + private val finalPalette: List = genColors() + + private data class RGB(val r: Int, val g: Int, val b: Int) + + fun usernameToColor(username: String): Int { + var hash = username.lowercase() + + val md5Regex = Regex("^([0-9a-f]{4}-?){8}$") + if (!hash.matches(md5Regex)) { + val digest = MessageDigest.getInstance("MD5").digest(hash.toByteArray(Charsets.UTF_8)) + hash = digest.joinToString("") { "%02x".format(it) } + } + + hash = hash.replace(Regex("[^0-9a-f]"), "") + + val idx = hashToInt(hash, finalPalette.size) + + val rgb = finalPalette[idx] + return Color.rgb(rgb.r, rgb.g, rgb.b) + } + + private fun hashToInt(hash: String, maximum: Int): Int { + val sum = hash.map { it.lowercaseChar().digitToInt(16) % 16 }.sum() + return sum % maximum + } + + private fun genColors(): List { + val red = RGB(182, 70, 157) + val yellow = RGB(221, 203, 85) + val blue = RGB(0, 130, 201) + + return mixPalette(red, yellow) + mixPalette(yellow, blue) + mixPalette(blue, red) + } + + private fun mixPalette(start: RGB, end: RGB): List { + val palette = mutableListOf(start) + val rStep = (end.r - start.r).toFloat() / STEPS + val gStep = (end.g - start.g).toFloat() / STEPS + val bStep = (end.b - start.b).toFloat() / STEPS + + for (i in 1 until STEPS) { + val r = (start.r + rStep * i).toInt().coerceIn(0, 255) + val g = (start.g + gStep * i).toInt().coerceIn(0, 255) + val b = (start.b + bStep * i).toInt().coerceIn(0, 255) + palette.add(RGB(r, g, b)) + } + return palette + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt new file mode 100644 index 0000000..74f1f24 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ContactUtils.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.Context +import android.provider.ContactsContract + +object ContactUtils { + + const val MAX_CONTACT_LIMIT = 50 + const val CACHE_MEMORY_SIZE_PERCENTAGE = 0.1 + const val CACHE_DISK_SIZE_PERCENTAGE = 0.02 + + fun getDisplayNameFromDeviceContact(context: Context, id: String?): String? { + var displayName: String? = null + val whereName = + ContactsContract.Data.MIMETYPE + + " = ? AND " + + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + + " = ?" + val whereNameParams = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id) + val nameCursor = context.contentResolver.query( + ContactsContract.Data.CONTENT_URI, + null, + whereName, + whereNameParams, + ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME + ) + if (nameCursor != null) { + while (nameCursor.moveToNext()) { + displayName = + nameCursor.getString( + nameCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME) + ) + } + nameCursor.close() + } + return displayName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ContextExtensions.kt b/app/src/main/java/com/nextcloud/talk/utils/ContextExtensions.kt new file mode 100644 index 0000000..1be6675 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ContextExtensions.kt @@ -0,0 +1,39 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.Handler + +@SuppressLint("UnspecifiedRegisterReceiverFlag") +fun Context.registerBroadcastReceiver(receiver: BroadcastReceiver?, filter: IntentFilter, flag: ReceiverFlag): Intent? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(receiver, filter, flag.value) + } else { + registerReceiver(receiver, filter) + } + +@SuppressLint("UnspecifiedRegisterReceiverFlag") +fun Context.registerPermissionHandlerBroadcastReceiver( + receiver: BroadcastReceiver?, + filter: IntentFilter, + broadcastPermission: String?, + scheduler: Handler?, + flag: ReceiverFlag +): Intent? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(receiver, filter, broadcastPermission, scheduler, flag.value) + } else { + registerReceiver(receiver, filter, broadcastPermission, scheduler) + } diff --git a/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt new file mode 100644 index 0000000..3142709 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ConversationUtils.kt @@ -0,0 +1,61 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant + +object ConversationUtils { + private val TAG = ConversationUtils::class.java.simpleName + + fun isPublic(conversation: ConversationModel): Boolean = + ConversationEnums.ConversationType.ROOM_PUBLIC_CALL == conversation.type + + fun isGuest(conversation: ConversationModel): Boolean = + Participant.ParticipantType.GUEST == conversation.participantType || + Participant.ParticipantType.GUEST_MODERATOR == conversation.participantType || + Participant.ParticipantType.USER_FOLLOWING_LINK == conversation.participantType + + fun isParticipantOwnerOrModerator(conversation: ConversationModel): Boolean = + Participant.ParticipantType.OWNER == conversation.participantType || + Participant.ParticipantType.GUEST_MODERATOR == conversation.participantType || + Participant.ParticipantType.MODERATOR == conversation.participantType + + fun isLockedOneToOne(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean = + conversation.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && + CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.LOCKED_ONE_TO_ONE) + + fun canModerate(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean = + isParticipantOwnerOrModerator(conversation) && + !isLockedOneToOne(conversation, spreedCapabilities) && + conversation.type != ConversationEnums.ConversationType.FORMER_ONE_TO_ONE && + !isNoteToSelfConversation(conversation) + + fun isConversationReadOnlyAvailable( + conversation: ConversationModel, + spreedCapabilities: SpreedCapability + ): Boolean = + CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS) && + canModerate(conversation, spreedCapabilities) + + fun isLobbyViewApplicable(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean = + !canModerate(conversation, spreedCapabilities) && + ( + conversation.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL || + conversation.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL + ) + + fun isNameEditable(conversation: ConversationModel, spreedCapabilities: SpreedCapability): Boolean = + canModerate(conversation, spreedCapabilities) && + ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL != conversation.type + + fun isNoteToSelfConversation(currentConversation: ConversationModel?): Boolean = + currentConversation != null && + currentConversation.type == ConversationEnums.ConversationType.NOTE_TO_SELF +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/DateConstants.kt b/app/src/main/java/com/nextcloud/talk/utils/DateConstants.kt new file mode 100644 index 0000000..1184090 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/DateConstants.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +class DateConstants { + companion object { + const val SECOND_DIVIDER = 1000 + const val MINUTES_DIVIDER = 60 + const val HOURS_DIVIDER = 60 + const val DAYS_DIVIDER = 24 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt new file mode 100644 index 0000000..ea5ba90 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/DateUtils.kt @@ -0,0 +1,106 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.Context +import android.content.res.Resources +import android.icu.text.RelativeDateTimeFormatter +import android.icu.text.RelativeDateTimeFormatter.Direction +import android.icu.text.RelativeDateTimeFormatter.RelativeUnit +import com.nextcloud.talk.R +import java.text.DateFormat +import java.util.Calendar +import java.util.Date +import kotlin.math.abs +import kotlin.math.roundToInt + +class DateUtils(val context: Context) { + private val cal = Calendar.getInstance() + private val tz = cal.timeZone + + /* date formatter in local timezone and locale */ + private var format: DateFormat = DateFormat.getDateTimeInstance( + // dateStyle + DateFormat.DEFAULT, + // timeStyle + DateFormat.SHORT, + context.resources.configuration.locales[0] + ) + + /* date formatter in local timezone and locale */ + private var formatTime: DateFormat = DateFormat.getTimeInstance( + // timeStyle + DateFormat.SHORT, + context.resources.configuration.locales[0] + ) + + init { + format.timeZone = tz + formatTime.timeZone = tz + } + + fun getLocalDateTimeStringFromTimestamp(timestampMilliseconds: Long): String = + format.format(Date(timestampMilliseconds)) + + fun getLocalTimeStringFromTimestamp(timestampSeconds: Long): String = + formatTime.format(Date(timestampSeconds * DateConstants.SECOND_DIVIDER)) + + fun isSameDate(date1: Date, date2: Date): Boolean { + val startDateCalendar = Calendar.getInstance().apply { time = date1 } + val endDateCalendar = Calendar.getInstance().apply { time = date2 } + val isSameDay = startDateCalendar.get(Calendar.YEAR) == endDateCalendar.get(Calendar.YEAR) && + startDateCalendar.get(Calendar.DAY_OF_YEAR) == endDateCalendar.get(Calendar.DAY_OF_YEAR) + return isSameDay + } + + fun getTimeDifferenceInSeconds(time2: Long, time1: Long): Long { + val difference = (time2 - time1) + return abs(difference) + } + + fun relativeStartTimeForLobby(timestampMilliseconds: Long, resources: Resources): String { + val fmt = RelativeDateTimeFormatter.getInstance() + val timeLeftMillis = timestampMilliseconds - System.currentTimeMillis() + val minutes = timeLeftMillis.toDouble() / DateConstants.SECOND_DIVIDER / DateConstants.MINUTES_DIVIDER + val hours = minutes / DateConstants.HOURS_DIVIDER + val days = hours / DateConstants.DAYS_DIVIDER + + val minutesInt = minutes.roundToInt() + val hoursInt = hours.roundToInt() + val daysInt = days.roundToInt() + + return when { + daysInt > 0 -> { + fmt.format( + daysInt.toDouble(), + Direction.NEXT, + RelativeUnit.DAYS + ) + } + + hoursInt > 0 -> { + fmt.format( + hoursInt.toDouble(), + Direction.NEXT, + RelativeUnit.HOURS + ) + } + + minutesInt > 1 -> { + fmt.format( + minutesInt.toDouble(), + Direction.NEXT, + RelativeUnit.MINUTES + ) + } + + else -> { + resources.getString(R.string.nc_lobby_start_soon) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/DeviceUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DeviceUtils.java new file mode 100644 index 0000000..6cdfc12 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/DeviceUtils.java @@ -0,0 +1,79 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.util.Log; +import com.nextcloud.talk.application.NextcloudTalkApplication; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +public class DeviceUtils { + private static final String TAG = "DeviceUtils"; + + public static void ignoreSpecialBatteryFeatures() { + if ("xiaomi".equalsIgnoreCase(Build.MANUFACTURER) || "meizu".equalsIgnoreCase(Build.MANUFACTURER)) { + try { + @SuppressLint("PrivateApi") Class appOpsUtilsClass = Class.forName("android.miui.AppOpsUtils"); + if (appOpsUtilsClass != null) { + Method setApplicationAutoStartMethod = appOpsUtilsClass.getMethod("setApplicationAutoStart", Context + .class, String.class, Boolean.TYPE); + if (setApplicationAutoStartMethod != null) { + Context applicationContext = NextcloudTalkApplication + .Companion + .getSharedApplication() + .getApplicationContext(); + setApplicationAutoStartMethod.invoke(appOpsUtilsClass, applicationContext, applicationContext + .getPackageName(), Boolean.TRUE); + } + } + } catch (ClassNotFoundException e) { + Log.e(TAG, "Class not found"); + } catch (NoSuchMethodException e) { + Log.e(TAG, "No such method"); + } catch (IllegalAccessException e) { + Log.e(TAG, "IllegalAccessException"); + } catch (InvocationTargetException e) { + Log.e(TAG, "InvocationTargetException"); + } + } else if ("huawei".equalsIgnoreCase(Build.MANUFACTURER)) { + try { + @SuppressLint("PrivateApi") Class protectAppControlClass = Class.forName( + "com.huawei.systemmanager.optimize.process.ProtectAppControl"); + if (protectAppControlClass != null) { + Context applicationContext = NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext(); + + Method getInstanceMethod = protectAppControlClass.getMethod("getInstance", Context.class); + // ProtectAppControl instance + if (getInstanceMethod != null) { + Object protectAppControlInstance = getInstanceMethod.invoke(null, applicationContext); + + Method setProtectMethod = protectAppControlClass.getDeclaredMethod("setProtect", List.class); + if (setProtectMethod != null) { + List appsList = new ArrayList<>(); + appsList.add(applicationContext.getPackageName()); + setProtectMethod.invoke(protectAppControlInstance, appsList); + } + } + } + } catch (ClassNotFoundException e) { + Log.e(TAG, "Class not found"); + } catch (NoSuchMethodException e) { + Log.e(TAG, "No such method"); + } catch (IllegalAccessException e) { + Log.e(TAG, "IllegalAccessException"); + } catch (InvocationTargetException e) { + Log.e(TAG, "InvocationTargetException"); + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt new file mode 100644 index 0000000..857adf0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt @@ -0,0 +1,538 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2020 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.text.Spannable +import android.text.SpannableString +import android.text.Spanned +import android.text.TextPaint +import android.text.TextUtils +import android.text.format.DateUtils +import android.text.method.LinkMovementMethod +import android.text.style.AbsoluteSizeSpan +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.util.TypedValue +import android.view.View +import android.view.Window +import android.widget.EditText +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.annotation.XmlRes +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.net.toUri +import androidx.emoji2.text.EmojiCompat +import coil.Coil.imageLoader +import coil.request.ImageRequest +import coil.target.Target +import coil.transform.CircleCropTransformation +import com.google.android.material.chip.ChipDrawable +import com.nextcloud.talk.PhoneUtils.isPhoneNumber +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.events.UserMentionClickEvent +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.ApiUtils.getUrlForAvatar +import com.nextcloud.talk.utils.ApiUtils.getUrlForFederatedAvatar +import com.nextcloud.talk.utils.ApiUtils.getUrlForGuestAvatar +import com.nextcloud.talk.utils.text.Spans.MentionChipSpan +import org.greenrobot.eventbus.EventBus +import third.parties.fresco.BetterImageSpan +import java.text.DateFormat +import java.util.Date +import java.util.regex.Pattern + +object DisplayUtils { + private val TAG = DisplayUtils::class.java.getSimpleName() + private const val INDEX_LUMINATION = 2 + private const val HSL_SIZE = 3 + private const val MAX_LIGHTNESS = 0.92 + private const val TWITTER_HANDLE_PREFIX = "@" + private const val HTTP_PROTOCOL = "http://" + private const val HTTPS_PROTOCOL = "https://" + private const val HTTP_MIN_LENGTH: Int = 7 + private const val HTTPS_MIN_LENGTH: Int = 7 + private const val DATE_TIME_PARTS_SIZE = 2 + private const val ONE_MINUTE_IN_MILLIS: Int = 60000 + private const val ROUND_UP_BUMP: Float = 0.5f + fun isDarkModeOn(context: Context): Boolean { + val currentNightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return currentNightMode == Configuration.UI_MODE_NIGHT_YES + } + + fun setClickableString(string: String, url: String, textView: TextView) { + val spannableString = SpannableString(string) + spannableString.setSpan( + object : ClickableSpan() { + override fun onClick(widget: View) { + val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + sharedApplication!!.applicationContext.startActivity(browserIntent) + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.isUnderlineText = false + } + }, + 0, + string.length, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + textView.text = spannableString + textView.movementMethod = LinkMovementMethod.getInstance() + } + + fun getBitmap(drawable: Drawable): Bitmap { + val bitmap = createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap + } + + @JvmStatic + fun convertDpToPixel(dp: Float, context: Context): Float = + Math.round( + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp, + context.resources.displayMetrics + ) + ROUND_UP_BUMP + ).toFloat() + + fun convertPixelToDp(px: Float, context: Context): Float = px / context.resources.displayMetrics.density + + fun getTintedDrawable(res: Resources, @DrawableRes drawableResId: Int, @ColorRes colorResId: Int): Drawable? { + val drawable = ResourcesCompat.getDrawable(res, drawableResId, null) + val color = res.getColor(colorResId) + drawable?.setTint(color) + return drawable + } + + @JvmStatic + fun getDrawableForMentionChipSpan( + context: Context, + id: String?, + roomToken: String?, + label: CharSequence, + conversationUser: User, + type: String, + @XmlRes chipResource: Int, + emojiEditText: EditText?, + viewThemeUtils: ViewThemeUtils, + isFederated: Boolean + ): Drawable { + val chip = ChipDrawable.createFromResource(context, chipResource) + chip.text = EmojiCompat.get().process(label) + chip.ellipsize = TextUtils.TruncateAt.MIDDLE + if (chipResource == R.xml.chip_you) { + viewThemeUtils.material.colorChipDrawable(context, chip) + } + val config = context.resources.configuration + chip.setLayoutDirection(config.layoutDirection) + val drawable: Int + val isCall = "call" == type || "calls" == type + val isGroup = "groups" == type || "user-group" == type + if (!isGroup && !isCall) { + drawable = if (chipResource == R.xml.chip_you) { + R.drawable.mention_chip + } else { + R.drawable.accent_circle + } + chip.setChipIconResource(drawable) + } else { + chip.setChipIconResource(R.drawable.ic_circular_group_mentions) + } + if (type == "circle" || type == "teams") { + chip.setChipIconResource(R.drawable.icon_circular_team) + } + + if (isCall && isPhoneNumber(label.toString())) { + chip.setChipIconResource(R.drawable.icon_circular_phone) + } + chip.setBounds(0, 0, chip.intrinsicWidth, chip.intrinsicHeight) + if (!isGroup) { + var url = getUrlForAvatar(conversationUser.baseUrl, id, false) + if ("guests" == type || "guest" == type || "email" == type) { + url = getUrlForGuestAvatar( + conversationUser.baseUrl, + label.toString(), + true + ) + } + if (isFederated) { + val darkTheme = if (isDarkModeOn(context)) 1 else 0 + url = getUrlForFederatedAvatar( + conversationUser.baseUrl!!, + roomToken!!, + id!!, + darkTheme, + false + ) + } + val imageRequest: ImageRequest = ImageRequest.Builder(context) + .data(url) + .crossfade(true) + .transformations(CircleCropTransformation()) + .target(object : Target { + override fun onStart(placeholder: Drawable?) { + // unused atm + } + override fun onError(error: Drawable?) { + chip.chipIcon = error + } + + override fun onSuccess(result: Drawable) { + chip.chipIcon = result + // A hack to refresh the chip icon + emojiEditText?.post { + emojiEditText.setTextKeepState( + emojiEditText.getText(), + TextView.BufferType.SPANNABLE + ) + } + } + }) + .build() + imageLoader(context).enqueue(imageRequest) + } + return chip + } + + fun searchAndReplaceWithMentionSpan( + key: String, + context: Context, + text: Spanned, + id: String, + roomToken: String?, + label: String, + type: String, + conversationUser: User, + @XmlRes chipXmlRes: Int, + viewThemeUtils: ViewThemeUtils, + isFederated: Boolean + ): Spannable { + val spannableString: Spannable = SpannableString(text) + val stringText = text.toString() + val keyWithBrackets = "{$key}" + val m = Pattern.compile(keyWithBrackets, Pattern.CASE_INSENSITIVE or Pattern.LITERAL or Pattern.MULTILINE) + .matcher(spannableString) + val clickableSpan: ClickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + EventBus.getDefault().post(UserMentionClickEvent(id)) + } + } + var lastStartIndex = 0 + var mentionChipSpan: MentionChipSpan + while (m.find()) { + val start = stringText.indexOf(m.group(), lastStartIndex) + val end = start + m.group().length + lastStartIndex = end + val drawableForChip = getDrawableForMentionChipSpan( + context, + id, + roomToken, + label, + conversationUser, + type, + chipXmlRes, + null, + viewThemeUtils, + isFederated + ) + mentionChipSpan = MentionChipSpan( + drawableForChip, + BetterImageSpan.ALIGN_CENTER, + id, + label + ) + spannableString.setSpan(mentionChipSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + if (chipXmlRes == R.xml.chip_you) { + spannableString.setSpan( + viewThemeUtils.talk.themeForegroundColorSpan(context), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + if ("user" == type && conversationUser.userId != id && !isFederated) { + spannableString.setSpan(clickableSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + } + } + return spannableString + } + + fun searchAndColor(text: Spannable, searchText: String, @ColorInt color: Int, textSize: Int): Spannable { + val spannableString: Spannable = SpannableString(text) + val stringText = text.toString() + if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) { + return spannableString + } + val m = Pattern.compile( + searchText, + Pattern.CASE_INSENSITIVE or Pattern.LITERAL or Pattern.MULTILINE + ) + .matcher(spannableString) + var lastStartIndex = -1 + while (m.find()) { + val start = stringText.indexOf(m.group(), lastStartIndex) + val end = start + m.group().length + lastStartIndex = end + spannableString.setSpan( + ForegroundColorSpan(color), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + spannableString.setSpan(StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spannableString.setSpan(AbsoluteSizeSpan(textSize), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return spannableString + } + + fun getMessageSelector( + @ColorInt normalColor: Int, + @ColorInt selectedColor: Int, + @ColorInt pressedColor: Int, + @DrawableRes shape: Int + ): Drawable { + val vectorDrawable = ContextCompat.getDrawable( + sharedApplication!!.applicationContext, + shape + ) + val drawable = DrawableCompat.wrap(vectorDrawable!!).mutate() + DrawableCompat.setTintList( + drawable, + ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_selected), + intArrayOf(android.R.attr.state_pressed), + intArrayOf(-android.R.attr.state_pressed, -android.R.attr.state_selected) + ), + intArrayOf(selectedColor, pressedColor, normalColor) + ) + ) + return drawable + } + + /** + * Sets the color of the status bar to `color`. + * + * @param activity activity + * @param color the color + */ + fun applyColorToStatusBar(activity: Activity, @ColorInt color: Int) { + val window = activity.window + val isLightTheme = lightTheme(color) + if (window != null) { + val decor = window.decorView + if (isLightTheme) { + val systemUiFlagLightStatusBar: Int = + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or + View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + + decor.systemUiVisibility = systemUiFlagLightStatusBar + } else { + decor.systemUiVisibility = 0 + } + window.statusBarColor = color + } + } + + /** + * Tests if light color is set + * + * @param color the color + * @return true if primaryColor is lighter than MAX_LIGHTNESS + */ + fun lightTheme(color: Int): Boolean { + val hsl = colorToHSL(color) + + // spotbugs dislikes fixed index access + // which is enforced by having such an + // array from Android-API itself + return hsl[INDEX_LUMINATION] >= MAX_LIGHTNESS + } + + private fun colorToHSL(color: Int): FloatArray { + val hsl = FloatArray(HSL_SIZE) + ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl) + return hsl + } + + fun applyColorToNavigationBar(window: Window, @ColorInt color: Int) { + window.navigationBarColor = color + } + + /** + * beautifies a given URL by removing any http/https protocol prefix. + * + * @param url to be beautified url + * @return beautified url + */ + @Suppress("ReturnCount") + fun beautifyURL(url: String?): String { + if (TextUtils.isEmpty(url)) { + return "" + } + if (url!!.length >= HTTP_MIN_LENGTH && + HTTP_PROTOCOL.equals(url.substring(0, HTTP_MIN_LENGTH), ignoreCase = true) + ) { + return url.substring(HTTP_PROTOCOL.length).trim() + } + return if (url.length >= HTTPS_MIN_LENGTH && + HTTPS_PROTOCOL.equals(url.substring(0, HTTPS_MIN_LENGTH), ignoreCase = true) + ) { + url.substring(HTTPS_PROTOCOL.length).trim() + } else { + url.trim() + } + } + + /** + * beautifies a given twitter handle by prefixing it with an @ in case it is missing. + * + * @param handle to be beautified twitter handle + * @return beautified twitter handle + */ + fun beautifyTwitterHandle(handle: String?): String { + return if (handle != null) { + val trimmedHandle = handle.trim() + if (TextUtils.isEmpty(trimmedHandle)) { + return "" + } + if (trimmedHandle.startsWith(TWITTER_HANDLE_PREFIX)) { + trimmedHandle + } else { + TWITTER_HANDLE_PREFIX + trimmedHandle + } + } else { + "" + } + } + + fun loadAvatarImage(user: User?, avatarImageView: ImageView?, deleteCache: Boolean) { + if (user != null && avatarImageView != null) { + val avatarId: String? = if (!TextUtils.isEmpty(user.userId)) { + user.userId + } else { + user.username + } + if (avatarId != null) { + avatarImageView.loadUserAvatar(user, avatarId, true, deleteCache) + } + } + } + + @StringRes + fun getSortOrderStringId(sortOrder: FileSortOrder): Int = + when (sortOrder.name) { + FileSortOrder.SORT_Z_TO_A_ID -> R.string.menu_item_sort_by_name_z_a + FileSortOrder.SORT_NEW_TO_OLD_ID -> R.string.menu_item_sort_by_date_newest_first + FileSortOrder.SORT_OLD_TO_NEW_ID -> R.string.menu_item_sort_by_date_oldest_first + FileSortOrder.SORT_BIG_TO_SMALL_ID -> R.string.menu_item_sort_by_size_biggest_first + FileSortOrder.SORT_SMALL_TO_BIG_ID -> R.string.menu_item_sort_by_size_smallest_first + FileSortOrder.SORT_A_TO_Z_ID -> R.string.menu_item_sort_by_name_a_z + else -> R.string.menu_item_sort_by_name_a_z + } + + /** + * calculates the relative time string based on the given modification timestamp. + * + * @param context the app's context + * @param modificationTimestamp the UNIX timestamp of the file modification time in milliseconds. + * @return a relative time string + */ + fun getRelativeTimestamp(context: Context, modificationTimestamp: Long, showFuture: Boolean): CharSequence = + getRelativeDateTimeString( + context, + modificationTimestamp, + DateUtils.SECOND_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0, + showFuture + ) + + @Suppress("ReturnCount") + private fun getRelativeDateTimeString( + c: Context, + time: Long, + minResolution: Long, + transitionResolution: Long, + flags: Int, + showFuture: Boolean + ): CharSequence { + val dateString: CharSequence + + // in Future + if (!showFuture && time > System.currentTimeMillis()) { + return unixTimeToHumanReadable(time) + } + // < 60 seconds -> seconds ago + val diff = System.currentTimeMillis() - time + dateString = if (diff in 1.. maxLength) { + text.substring(0, maxLength - 1) + "…" + } else { + text + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/DoNotDisturbUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DoNotDisturbUtils.kt new file mode 100644 index 0000000..29a13ab --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/DoNotDisturbUtils.kt @@ -0,0 +1,47 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.annotation.SuppressLint +import android.app.NotificationManager +import android.content.Context +import android.media.AudioManager +import android.os.Build +import androidx.annotation.VisibleForTesting +import com.nextcloud.talk.application.NextcloudTalkApplication + +object DoNotDisturbUtils { + + private var buildVersion = Build.VERSION.SDK_INT + + @VisibleForTesting + fun setTestingBuildVersion(version: Int) { + buildVersion = version + } + + @SuppressLint("NewApi") + @JvmOverloads + fun shouldPlaySound(context: Context? = NextcloudTalkApplication.sharedApplication?.applicationContext): Boolean { + val notificationManager = context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + + var shouldPlaySound = true + if (buildVersion >= Build.VERSION_CODES.M) { + if (notificationManager.currentInterruptionFilter != NotificationManager.INTERRUPTION_FILTER_ALL) { + shouldPlaySound = false + } + } + + if (shouldPlaySound) { + if (audioManager.ringerMode != AudioManager.RINGER_MODE_NORMAL) { + shouldPlaySound = false + } + } + + return shouldPlaySound + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/DrawableUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DrawableUtils.kt new file mode 100644 index 0000000..827cb55 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/DrawableUtils.kt @@ -0,0 +1,183 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.graphics.drawable.Drawable +import android.graphics.drawable.RippleDrawable +import android.util.Log +import com.nextcloud.talk.R +import com.nextcloud.talk.utils.Mimetype.AUDIO_PREFIX +import com.nextcloud.talk.utils.Mimetype.FOLDER +import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX +import com.nextcloud.talk.utils.Mimetype.TEXT_PREFIX +import com.nextcloud.talk.utils.Mimetype.VIDEO_PREFIX +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +object DrawableUtils { + private val TAG = DrawableUtils::class.java.simpleName + + @Suppress("Detekt.LongMethod") + fun getDrawableResourceIdForMimeType(mimetype: String?): Int { + var localMimetype = mimetype + val drawableMap = HashMap() + + // Initial list of mimetypes was acquired from https://github.com/nextcloud/server/blob/694ba5435b2963e201f6a6d2c775836bde07aaef/core/js/mimetypelist.js + drawableMap["application/coreldraw"] = R.drawable.ic_mimetype_image + drawableMap["application/epub+zip"] = R.drawable.ic_mimetype_text + drawableMap["application/font-sfnt"] = R.drawable.ic_mimetype_image + drawableMap["application/font-woff"] = R.drawable.ic_mimetype_image + drawableMap["application/gpx+xml"] = R.drawable.ic_mimetype_location + drawableMap["application/illustrator"] = R.drawable.ic_mimetype_image + drawableMap["application/javascript"] = R.drawable.ic_mimetype_text_code + drawableMap["application/json"] = R.drawable.ic_mimetype_text_code + drawableMap["application/msaccess"] = R.drawable.ic_mimetype_file + drawableMap["application/msexcel"] = R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/msonenote"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/mspowerpoint"] = R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/msword"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/octet-stream"] = R.drawable.ic_mimetype_file + drawableMap["application/postscript"] = R.drawable.ic_mimetype_image + drawableMap["application/rss+xml"] = R.drawable.ic_mimetype_text_code + drawableMap["application/vnd.android.package-archive"] = R.drawable.ic_mimetype_package_x_generic + drawableMap["application/vnd.lotus-wordpro"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.garmin.tcx+xml"] = R.drawable.ic_mimetype_location + drawableMap["application/vnd.google-earth.kml+xml"] = R.drawable.ic_mimetype_location + drawableMap["application/vnd.google-earth.kmz"] = R.drawable.ic_mimetype_location + drawableMap["application/vnd.ms-excel"] = R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.ms-excel.addin.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.ms-excel.sheet.binary.macroEnabled.12"] = + R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.ms-excel.sheet.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.ms-excel.template.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.ms-fontobject"] = R.drawable.ic_mimetype_image + drawableMap["application/vnd.ms-powerpoint"] = R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.ms-powerpoint.addin.macroEnabled.12"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.ms-powerpoint.presentation.macroEnabled.12"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.ms-powerpoint.slideshow.macroEnabled.12"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.ms-powerpoint.template.macroEnabled.12"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.ms-visio.drawing.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.ms-visio.drawing"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.ms-visio.stencil.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.ms-visio.stencil"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.ms-visio.template.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.ms-visio.template"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.ms-word.template.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.oasis.opendocument.presentation"] = R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.oasis.opendocument.presentation-template"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.oasis.opendocument.spreadsheet"] = R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.oasis.opendocument.spreadsheet-template"] = + R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.oasis.opendocument.text"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.oasis.opendocument.text-master"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.oasis.opendocument.text-template"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.oasis.opendocument.text-web"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.openxmlformats-officedocument.presentationml.presentation"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.openxmlformats-officedocument.presentationml.slideshow"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.openxmlformats-officedocument.presentationml.template"] = + R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] = + R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.openxmlformats-officedocument.spreadsheetml.template"] = + R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] = + R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.openxmlformats-officedocument.wordprocessingml.template"] = + R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.visio"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/vnd.wordperfect"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/x-7z-compressed"] = R.drawable.ic_mimetype_package_x_generic + drawableMap["application/x-bzip2"] = R.drawable.ic_mimetype_package_x_generic + drawableMap["application/x-cbr"] = R.drawable.ic_mimetype_text + drawableMap["application/x-compressed"] = R.drawable.ic_mimetype_package_x_generic + drawableMap["application/x-dcraw"] = R.drawable.ic_mimetype_image + drawableMap["application/x-deb"] = R.drawable.ic_mimetype_package_x_generic + drawableMap["application/x-fictionbook+xml"] = R.drawable.ic_mimetype_text + drawableMap["application/x-font"] = R.drawable.ic_mimetype_image + drawableMap["application/x-gimp"] = R.drawable.ic_mimetype_image + drawableMap["application/x-gzip"] = R.drawable.ic_mimetype_package_x_generic + drawableMap["application/x-iwork-keynote-sffkey"] = R.drawable.ic_mimetype_x_office_presentation + drawableMap["application/x-iwork-numbers-sffnumbers"] = R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["application/x-iwork-pages-sffpages"] = R.drawable.ic_mimetype_x_office_document + drawableMap["application/x-mobipocket-ebook"] = R.drawable.ic_mimetype_text + drawableMap["application/x-perl"] = R.drawable.ic_mimetype_text_code + drawableMap["application/x-photoshop"] = R.drawable.ic_mimetype_image + drawableMap["application/x-php"] = R.drawable.ic_mimetype_text_code + drawableMap["application/x-rar-compressed"] = R.drawable.ic_mimetype_package_x_generic + drawableMap["application/x-tar"] = R.drawable.ic_mimetype_package_x_generic + drawableMap["application/x-tex"] = R.drawable.ic_mimetype_text + drawableMap["application/xml"] = R.drawable.ic_mimetype_text_code + drawableMap["application/yaml"] = R.drawable.ic_mimetype_text_code + drawableMap["application/zip"] = R.drawable.ic_mimetype_package_x_generic + drawableMap["database"] = R.drawable.ic_mimetype_file + drawableMap["httpd/unix-directory"] = R.drawable.ic_mimetype_folder + drawableMap["text/css"] = R.drawable.ic_mimetype_text_code + drawableMap["text/csv"] = R.drawable.ic_mimetype_x_office_spreadsheet + drawableMap["text/html"] = R.drawable.ic_mimetype_text_code + drawableMap["text/vcard"] = R.drawable.ic_mimetype_text_vcard + drawableMap["text/x-c"] = R.drawable.ic_mimetype_text_code + drawableMap["text/x-c++src"] = R.drawable.ic_mimetype_text_code + drawableMap["text/x-h"] = R.drawable.ic_mimetype_text_code + drawableMap["text/x-java-source"] = R.drawable.ic_mimetype_text_code + drawableMap["text/x-ldif"] = R.drawable.ic_mimetype_text_code + drawableMap["text/x-python"] = R.drawable.ic_mimetype_text_code + drawableMap["text/x-shellscript"] = R.drawable.ic_mimetype_text_code + drawableMap["web"] = R.drawable.ic_mimetype_text_code + drawableMap["application/internet-shortcut"] = R.drawable.ic_mimetype_link + + drawableMap[FOLDER] = R.drawable.ic_mimetype_folder + drawableMap["unknown"] = R.drawable.ic_mimetype_file + drawableMap["application/pdf"] = R.drawable.ic_mimetype_application_pdf + + return if (localMimetype.isNullOrEmpty()) { + drawableMap["unknown"]!! + } else if ("DIR" == localMimetype) { + localMimetype = FOLDER + drawableMap[localMimetype]!! + } else if (drawableMap.containsKey(localMimetype)) { + drawableMap[localMimetype]!! + } else if (localMimetype.startsWith(IMAGE_PREFIX)) { + R.drawable.ic_mimetype_image + } else if (localMimetype.startsWith(VIDEO_PREFIX)) { + R.drawable.ic_mimetype_video + } else if (localMimetype.startsWith(TEXT_PREFIX)) { + R.drawable.ic_mimetype_text + } else if (localMimetype.startsWith(AUDIO_PREFIX)) { + R.drawable.ic_mimetype_audio + } else { + drawableMap["unknown"]!! + } + } + + @Suppress("MagicNumber", "TooGenericExceptionCaught") + fun blinkDrawable(rippleView: Drawable) { + try { + (rippleView as RippleDrawable).let { rippleDrawable -> + CoroutineScope(Dispatchers.Main).launch { + delay(1000L) // Wait 2 seconds before starting + repeat(3) { + rippleDrawable.state = intArrayOf(android.R.attr.state_pressed, android.R.attr.state_enabled) + delay(250L) // Ripple active duration + rippleDrawable.state = intArrayOf() // Reset state + delay(250L) // Time between blinks + } + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to blink Drawable", e) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/EmojiTextInputEditText.java b/app/src/main/java/com/nextcloud/talk/utils/EmojiTextInputEditText.java new file mode 100644 index 0000000..fec7eee --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/EmojiTextInputEditText.java @@ -0,0 +1,59 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +import androidx.annotation.NonNull; +import androidx.emoji2.viewsintegration.EmojiEditTextHelper; + +import com.google.android.material.textfield.TextInputEditText; + +public class EmojiTextInputEditText extends TextInputEditText { + private EmojiEditTextHelper emojiEditTextHelper; + + public EmojiTextInputEditText(Context context) { + super(context); + init(); + } + + public EmojiTextInputEditText(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public EmojiTextInputEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + super.setKeyListener(getEmojiEditTextHelper().getKeyListener(getKeyListener())); + } + + @Override + public void setKeyListener(android.text.method.KeyListener keyListener) { + super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener)); + } + + @Override + public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) { + InputConnection inputConnection = super.onCreateInputConnection(outAttrs); + return getEmojiEditTextHelper().onCreateInputConnection(inputConnection, outAttrs); + } + + private EmojiEditTextHelper getEmojiEditTextHelper() { + if (emojiEditTextHelper == null) { + emojiEditTextHelper = new EmojiEditTextHelper(this); + } + + return emojiEditTextHelper; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FABAwareScrollingViewBehavior.java b/app/src/main/java/com/nextcloud/talk/utils/FABAwareScrollingViewBehavior.java new file mode 100644 index 0000000..5c5f81c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FABAwareScrollingViewBehavior.java @@ -0,0 +1,62 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2015 The Android Open Source Project + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nextcloud.talk.utils; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.util.List; + +public class FABAwareScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior { + + public FABAwareScrollingViewBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { + return super.layoutDependsOn(parent, child, dependency) || + dependency instanceof FloatingActionButton; + } + + @Override + public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final View child, + final View directTargetChild, final View target, final int nestedScrollAxes) { + // Ensure we react to vertical scrolling + return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL + || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes); + } + + @Override + public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final View child, + final View target, final int dxConsumed, final int dyConsumed, + final int dxUnconsumed, final int dyUnconsumed) { + super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); + if (dyConsumed > 0) { + // User scrolled down -> hide the FAB + List dependencies = coordinatorLayout.getDependencies(child); + for (View view : dependencies) { + if (view instanceof FloatingActionButton) { + ((FloatingActionButton) view).hide(); + } + } + } else if (dyConsumed < 0) { + // User scrolled up -> show the FAB + List dependencies = coordinatorLayout.getDependencies(child); + for (View view : dependencies) { + if (view instanceof FloatingActionButton) { + ((FloatingActionButton) view).show(); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt new file mode 100644 index 0000000..4bdb50b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrder.kt @@ -0,0 +1,78 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Sven R. Kunze + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.text.TextUtils +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import java.util.Collections + +open class FileSortOrder(var name: String, var isAscending: Boolean) { + companion object { + const val SORT_A_TO_Z_ID = "sort_a_to_z" + const val SORT_Z_TO_A_ID = "sort_z_to_a" + const val SORT_OLD_TO_NEW_ID = "sort_old_to_new" + const val SORT_NEW_TO_OLD_ID = "sort_new_to_old" + const val SORT_SMALL_TO_BIG_ID = "sort_small_to_big" + const val SORT_BIG_TO_SMALL_ID = "sort_big_to_small" + + val sort_a_to_z: FileSortOrder = FileSortOrderByName(SORT_A_TO_Z_ID, true) + val sort_z_to_a: FileSortOrder = FileSortOrderByName(SORT_Z_TO_A_ID, false) + val sort_old_to_new: FileSortOrder = FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true) + val sort_new_to_old: FileSortOrder = FileSortOrderByDate(SORT_NEW_TO_OLD_ID, false) + val sort_small_to_big: FileSortOrder = FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, true) + val sort_big_to_small: FileSortOrder = FileSortOrderBySize(SORT_BIG_TO_SMALL_ID, false) + + val sortOrders: Map = mapOf( + sort_a_to_z.name to sort_a_to_z, + sort_z_to_a.name to sort_z_to_a, + sort_old_to_new.name to sort_old_to_new, + sort_new_to_old.name to sort_new_to_old, + sort_small_to_big.name to sort_small_to_big, + sort_big_to_small.name to sort_big_to_small + ) + + fun getFileSortOrder(key: String?): FileSortOrder = + if (TextUtils.isEmpty(key) || !sortOrders.containsKey(key)) { + sort_a_to_z + } else { + sortOrders[key]!! + } + + /** + * Sorts list by Favourites. + * + * @param files files to sort + */ + fun sortCloudFilesByFavourite(files: List): List { + Collections.sort(files, RemoteFileBrowserItemFavoriteComparator()) + return files + } + } + + val multiplier: Int + get() = if (isAscending) 1 else -1 + + open fun sortCloudFiles(files: List): List = + sortCloudFilesByFavourite(files) + + /** + * Comparator for RemoteFileBrowserItems, sorts favorite state. + */ + class RemoteFileBrowserItemFavoriteComparator : Comparator { + override fun compare(left: RemoteFileBrowserItem, right: RemoteFileBrowserItem): Int = + if (left.isFavorite && right.isFavorite) { + 0 + } else if (left.isFavorite) { + -1 + } else if (right.isFavorite) { + 1 + } else { + 0 + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDate.kt b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDate.kt new file mode 100644 index 0000000..e4e56bb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDate.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Sven R. Kunze + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import java.util.Collections + +class FileSortOrderByDate internal constructor(name: String, ascending: Boolean) : FileSortOrder(name, ascending) { + /** + * Sorts list by Date. + * + * @param files list of files to sort + */ + override fun sortCloudFiles(files: List): List { + Collections.sort(files, RemoteFileBrowserItemDateComparator(multiplier)) + return super.sortCloudFiles(files) + } + + /** + * Comparator for RemoteFileBrowserItems, sorts by modified timestamp. + */ + class RemoteFileBrowserItemDateComparator(private val multiplier: Int) : Comparator { + + override fun compare(left: RemoteFileBrowserItem, right: RemoteFileBrowserItem): Int = + multiplier * left.modifiedTimestamp.compareTo(right.modifiedTimestamp) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt new file mode 100644 index 0000000..b10a11d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByName.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Sven R. Kunze + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import third.parties.daveKoeller.AlphanumComparator +import java.util.Collections + +class FileSortOrderByName internal constructor(name: String, ascending: Boolean) : FileSortOrder(name, ascending) { + /** + * Sorts list by Name. + * + * @param files files to sort + */ + override fun sortCloudFiles(files: List): List { + Collections.sort(files, RemoteFileBrowserItemNameComparator(multiplier)) + return super.sortCloudFiles(files) + } + + /** + * Comparator for RemoteFileBrowserItems, sorts by name. + */ + class RemoteFileBrowserItemNameComparator(private val multiplier: Int) : Comparator { + private val alphanumComparator = + AlphanumComparator() + + override fun compare(left: RemoteFileBrowserItem, right: RemoteFileBrowserItem): Int { + return if (!left.isFile && !right.isFile) { + return multiplier * alphanumComparator.compare(left.path, right.path) + } else if (!left.isFile) { + -1 + } else if (!right.isFile) { + 1 + } else { + multiplier * alphanumComparator.compare(left.path, right.path) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySize.kt b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySize.kt new file mode 100644 index 0000000..25155f4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySize.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021-2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Sven R. Kunze + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem +import java.util.Collections + +class FileSortOrderBySize internal constructor(name: String, ascending: Boolean) : FileSortOrder(name, ascending) { + /** + * Sorts list by Size. + * + * @param files list of files to sort + */ + override fun sortCloudFiles(files: List): List { + Collections.sort(files, RemoteFileBrowserItemSizeComparator(multiplier)) + return super.sortCloudFiles(files) + } + + /** + * Comparator for RemoteFileBrowserItems, sorts by name. + */ + class RemoteFileBrowserItemSizeComparator(private val multiplier: Int) : Comparator { + + override fun compare(left: RemoteFileBrowserItem, right: RemoteFileBrowserItem): Int { + return if (!left.isFile && !right.isFile) { + return multiplier * left.size.compareTo(right.size) + } else if (!left.isFile) { + -1 + } else if (!right.isFile) { + 1 + } else { + multiplier * left.size.compareTo(right.size) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt new file mode 100644 index 0000000..ca7e54c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt @@ -0,0 +1,166 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Stefan Niedermann + * SPDX-FileCopyrightText: 2020 Chris Narkiewicz + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.provider.OpenableColumns +import android.util.Log +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.math.BigInteger +import java.security.MessageDigest + +object FileUtils { + private val TAG = FileUtils::class.java.simpleName + private const val RADIX: Int = 16 + private const val MD5_LENGTH: Int = 32 + + /** + * Creates a new [File] + */ + @Suppress("ThrowsCount") + @JvmStatic + fun getTempCacheFile(context: Context, fileName: String): File { + val cacheFile = File(context.applicationContext.filesDir.absolutePath + "/" + fileName) + Log.v(TAG, "Full path for new cache file:" + cacheFile.absolutePath) + val tempDir = cacheFile.parentFile ?: throw FileNotFoundException("could not cacheFile.getParentFile()") + if (!tempDir.exists()) { + Log.v( + TAG, + "The folder in which the new file should be created does not exist yet. Trying to create it…" + ) + if (tempDir.mkdirs()) { + Log.v(TAG, "Creation successful") + } else { + throw IOException("Directory for temporary file does not exist and could not be created.") + } + } + Log.v(TAG, "- Try to create actual cache file") + if (cacheFile.createNewFile()) { + Log.v(TAG, "Successfully created cache file") + } else { + throw IOException("Failed to create cacheFile") + } + return cacheFile + } + + /** + * Creates a new [File] + */ + fun removeTempCacheFile(context: Context, fileName: String) { + val cacheFile = File(context.applicationContext.filesDir.absolutePath + "/" + fileName) + Log.v(TAG, "Full path for new cache file:" + cacheFile.absolutePath) + if (cacheFile.exists()) { + if (cacheFile.delete()) { + Log.v(TAG, "Deletion successful") + } else { + throw IOException("Directory for temporary file does not exist and could not be created.") + } + } + } + + @Suppress("ThrowsCount") + fun getFileFromUri(context: Context, sourceFileUri: Uri): File? { + val fileName = getFileName(sourceFileUri, context) + val scheme = sourceFileUri.scheme + + val file = if (scheme == null) { + Log.d(TAG, "relative uri: " + sourceFileUri.path) + throw IllegalArgumentException("relative paths are not supported") + } else if (ContentResolver.SCHEME_CONTENT == scheme) { + copyFileToCache(context, sourceFileUri, fileName) + } else if (ContentResolver.SCHEME_FILE == scheme) { + if (sourceFileUri.path != null) { + sourceFileUri.path?.let { File(it) } + } else { + throw IllegalArgumentException("uri does not contain path") + } + } else { + throw IllegalArgumentException("unsupported scheme: " + sourceFileUri.path) + } + return file + } + + @Suppress("NestedBlockDepth") + fun copyFileToCache(context: Context, sourceFileUri: Uri, filename: String): File? { + val cachedFile = File(context.cacheDir, filename) + + if (!cachedFile.toPath().normalize().startsWith(context.cacheDir.toPath())) { + Log.w(TAG, "cachedFile was not created in cacheDir. Aborting for security reasons.") + cachedFile.delete() + return null + } + + if (cachedFile.exists()) { + Log.d(TAG, "file is already in cache") + } else { + val outputStream = FileOutputStream(cachedFile) + try { + val inputStream: InputStream? = context.contentResolver.openInputStream(sourceFileUri) + inputStream?.use { input -> + outputStream.use { output -> + input.copyTo(output) + } + } + outputStream.flush() + } catch (e: FileNotFoundException) { + Log.w(TAG, "failed to copy file to cache", e) + } + } + return cachedFile + } + + fun getFileName(uri: Uri, context: Context?): String { + var filename: String? = null + if (uri.scheme == "content" && context != null) { + val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null) + try { + if (cursor != null && cursor.moveToFirst()) { + val displayNameColumnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (displayNameColumnIndex != -1) { + filename = cursor.getString(displayNameColumnIndex) + } + } + } finally { + cursor?.close() + } + } + // if it was no content uri, read filename from path + if (filename == null) { + filename = uri.path + } + + val lastIndexOfSlash = filename!!.lastIndexOf('/') + if (lastIndexOfSlash != -1) { + filename = filename.substring(lastIndexOfSlash + 1) + } + + return filename + } + + @JvmStatic + fun md5Sum(file: File): String { + val temp = file.name + file.lastModified() + file.length() + val messageDigest = MessageDigest.getInstance("MD5") + messageDigest.update(temp.toByteArray()) + val digest = messageDigest.digest() + val md5String = StringBuilder(BigInteger(1, digest).toString(RADIX)) + while (md5String.length < MD5_LENGTH) { + md5String.insert(0, "0") + } + return md5String.toString() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt new file mode 100644 index 0000000..b906053 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt @@ -0,0 +1,411 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.annotation.SuppressLint +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.ProgressBar +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import androidx.emoji2.widget.EmojiTextView +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.fullscreenfile.FullScreenImageActivity +import com.nextcloud.talk.fullscreenfile.FullScreenMediaActivity +import com.nextcloud.talk.fullscreenfile.FullScreenTextViewerActivity +import com.nextcloud.talk.jobs.DownloadFileToCacheWorker +import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp +import com.nextcloud.talk.utils.Mimetype.AUDIO_MPEG +import com.nextcloud.talk.utils.Mimetype.AUDIO_OGG +import com.nextcloud.talk.utils.Mimetype.AUDIO_WAV +import com.nextcloud.talk.utils.Mimetype.IMAGE_GIF +import com.nextcloud.talk.utils.Mimetype.IMAGE_HEIC +import com.nextcloud.talk.utils.Mimetype.IMAGE_JPEG +import com.nextcloud.talk.utils.Mimetype.IMAGE_PNG +import com.nextcloud.talk.utils.Mimetype.TEXT_MARKDOWN +import com.nextcloud.talk.utils.Mimetype.TEXT_PLAIN +import com.nextcloud.talk.utils.Mimetype.VIDEO_MP4 +import com.nextcloud.talk.utils.Mimetype.VIDEO_OGG +import com.nextcloud.talk.utils.Mimetype.VIDEO_QUICKTIME +import com.nextcloud.talk.utils.Mimetype.VIDEO_WEBM +import com.nextcloud.talk.utils.MimetypeUtils.isAudioOnly +import com.nextcloud.talk.utils.MimetypeUtils.isGif +import com.nextcloud.talk.utils.MimetypeUtils.isMarkdown +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACCOUNT +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_ID +import java.io.File +import java.util.concurrent.ExecutionException + +/* + * Usage of this class forces us to do things at one location which should be separated in a activity and view model. + * + * Example: + * - SharedItemsViewHolder + */ +class FileViewerUtils(private val context: Context, private val user: User) { + + fun openFile(message: ChatMessage, progressUi: ProgressUi) { + val fileName = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_NAME]!! + val mimetype = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_MIMETYPE]!! + val link = message.selectedIndividualHashMap!!["link"]!! + + val fileId = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]!! + val path = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_PATH]!! + + var size = message.selectedIndividualHashMap!!["size"] + if (size == null) { + size = "-1" + } + val fileSize = size.toLong() + + openFile( + FileInfo(fileId, fileName, fileSize), + path, + link, + mimetype, + progressUi, + message.openWhenDownloaded + ) + } + + fun openFile( + fileInfo: FileInfo, + path: String, + link: String?, + mimetype: String?, + progressUi: ProgressUi, + openWhenDownloaded: Boolean + ) { + if (isSupportedForInternalViewer(mimetype) || canBeHandledByExternalApp(mimetype, fileInfo.fileName)) { + openOrDownloadFile( + fileInfo, + path, + mimetype, + progressUi, + openWhenDownloaded + ) + } else if (!link.isNullOrEmpty()) { + openFileInFilesApp(link, fileInfo.fileId) + } else { + Log.e( + TAG, + "File with id " + fileInfo.fileId + " can't be opened because internal viewer doesn't " + + "support it, it can't be handled by an external app and there is no link " + + "to open it in the nextcloud files app" + ) + Snackbar.make(View(context), R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + } + + private fun canBeHandledByExternalApp(mimetype: String?, fileName: String): Boolean { + val path: String = context.cacheDir.absolutePath + "/" + fileName + val file = File(path) + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(Uri.fromFile(file), mimetype) + return intent.resolveActivity(context.packageManager) != null + } + + private fun openOrDownloadFile( + fileInfo: FileInfo, + path: String, + mimetype: String?, + progressUi: ProgressUi, + openWhenDownloaded: Boolean + ) { + val file = File(context.cacheDir, fileInfo.fileName) + if (file.exists()) { + openFileByMimetype(fileInfo.fileName, mimetype) + } else { + downloadFileToCache( + fileInfo, + path, + mimetype, + progressUi, + openWhenDownloaded + ) + } + } + + private fun openFileByMimetype(filename: String, mimetype: String?) { + if (mimetype != null) { + when (mimetype) { + AUDIO_MPEG, + AUDIO_WAV, + AUDIO_OGG, + VIDEO_MP4, + VIDEO_QUICKTIME, + VIDEO_OGG, + VIDEO_WEBM + -> openMediaView(filename, mimetype) + IMAGE_PNG, + IMAGE_JPEG, + IMAGE_GIF, + IMAGE_HEIC + -> openImageView(filename, mimetype) + TEXT_MARKDOWN, + TEXT_PLAIN + -> openTextView(filename, mimetype) + else + -> openFileByExternalApp(filename, mimetype) + } + } else { + Log.e(TAG, "can't open file with unknown mimetype") + Snackbar.make(View(context), R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private fun openFileByExternalApp(fileName: String, mimetype: String) { + val path = context.cacheDir.absolutePath + "/" + fileName + val file = File(path) + val intent = Intent() + intent.action = Intent.ACTION_VIEW + val pdfURI = FileProvider.getUriForFile(context, context.packageName, file) + intent.setDataAndType(pdfURI, mimetype) + intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + try { + if (intent.resolveActivity(context.packageManager) != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } else { + Log.e(TAG, "No Application found to open the file. This should have been handled beforehand!") + } + } catch (e: Exception) { + Log.e(TAG, "Error while opening file", e) + } + } + + fun openFileInFilesApp(link: String, keyID: String) { + val accountString = user.username + "@" + + user.baseUrl + ?.replace("https://", "") + ?.replace("http://", "") + + if (canWeOpenFilesApp(context, accountString)) { + val filesAppIntent = Intent(Intent.ACTION_VIEW, null) + val componentName = ComponentName( + context.getString(R.string.nc_import_accounts_from), + "com.owncloud.android.ui.activity.FileDisplayActivity" + ) + filesAppIntent.component = componentName + filesAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + filesAppIntent.setPackage(context.getString(R.string.nc_import_accounts_from)) + filesAppIntent.putExtra(KEY_ACCOUNT, accountString) + filesAppIntent.putExtra(KEY_FILE_ID, keyID) + context.startActivity(filesAppIntent) + } else { + val browserIntent = Intent( + Intent.ACTION_VIEW, + link.toUri() + ) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(browserIntent) + } + } + + private fun openImageView(filename: String, mimetype: String) { + val fullScreenImageIntent = Intent(context, FullScreenImageActivity::class.java) + fullScreenImageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + fullScreenImageIntent.putExtra("FILE_NAME", filename) + fullScreenImageIntent.putExtra("IS_GIF", isGif(mimetype)) + context.startActivity(fullScreenImageIntent) + } + + private fun openMediaView(filename: String, mimetype: String) { + val fullScreenMediaIntent = Intent(context, FullScreenMediaActivity::class.java) + fullScreenMediaIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + fullScreenMediaIntent.putExtra("FILE_NAME", filename) + fullScreenMediaIntent.putExtra("AUDIO_ONLY", isAudioOnly(mimetype)) + context.startActivity(fullScreenMediaIntent) + } + + private fun openTextView(filename: String, mimetype: String) { + val fullScreenTextViewerIntent = Intent(context, FullScreenTextViewerActivity::class.java) + fullScreenTextViewerIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + fullScreenTextViewerIntent.putExtra("FILE_NAME", filename) + fullScreenTextViewerIntent.putExtra("IS_MARKDOWN", isMarkdown(mimetype)) + context.startActivity(fullScreenTextViewerIntent) + } + + fun isSupportedForInternalViewer(mimetype: String?): Boolean = + when (mimetype) { + IMAGE_PNG, + IMAGE_JPEG, + IMAGE_HEIC, + IMAGE_GIF, + AUDIO_MPEG, + AUDIO_WAV, + AUDIO_OGG, + VIDEO_MP4, + VIDEO_QUICKTIME, + VIDEO_OGG, + VIDEO_WEBM, + TEXT_MARKDOWN, + TEXT_PLAIN -> true + else -> false + } + + @SuppressLint("LongLogTag") + private fun downloadFileToCache( + fileInfo: FileInfo, + path: String, + mimetype: String?, + progressUi: ProgressUi, + openWhenDownloaded: Boolean + ) { + // check if download worker is already running + val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileInfo.fileId) + try { + for (workInfo in workers.get()) { + if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) { + Log.d(TAG, "Download worker for $fileInfo.fileId is already running or scheduled") + return + } + } + } catch (e: ExecutionException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } catch (e: InterruptedException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } + val downloadWorker: OneTimeWorkRequest + + val size: Long = if (fileInfo.fileSize == null) { + -1 + } else { + fileInfo.fileSize!! + } + + val data: Data = Data.Builder() + .putString(DownloadFileToCacheWorker.KEY_BASE_URL, user.baseUrl) + .putString(DownloadFileToCacheWorker.KEY_USER_ID, user.userId) + .putString( + DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, + CapabilitiesUtil.getAttachmentFolder(user.capabilities!!.spreedCapability!!) + ) + .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileInfo.fileName) + .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path) + .putLong(DownloadFileToCacheWorker.KEY_FILE_SIZE, size) + .build() + + downloadWorker = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java) + .setInputData(data) + .addTag(fileInfo.fileId) + .build() + WorkManager.getInstance().enqueue(downloadWorker) + progressUi.progressBar?.visibility = View.VISIBLE + WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id) + .observeForever { workInfo: WorkInfo? -> + updateViewsByProgress( + fileInfo.fileName, + mimetype, + workInfo!!, + progressUi, + openWhenDownloaded + ) + } + } + + private fun updateViewsByProgress( + fileName: String, + mimetype: String?, + workInfo: WorkInfo, + progressUi: ProgressUi, + openWhenDownloaded: Boolean + ) { + when (workInfo.state) { + WorkInfo.State.RUNNING -> { + val progress = workInfo.progress.getInt(DownloadFileToCacheWorker.PROGRESS, -1) + if (progress > -1) { + progressUi.messageText?.text = String.format( + context.resources.getString(R.string.filename_progress), + fileName, + progress + ) + } + } + WorkInfo.State.SUCCEEDED -> { + if (progressUi.previewImage.isShown && openWhenDownloaded) { + openFileByMimetype(fileName, mimetype) + } else { + Log.d( + TAG, + "file " + fileName + + " was downloaded but it's not opened because view is not shown on screen or " + + "openWhenDownloaded is false" + ) + } + progressUi.messageText?.text = fileName + progressUi.progressBar?.visibility = View.GONE + } + WorkInfo.State.FAILED -> { + progressUi.messageText?.text = fileName + progressUi.progressBar?.visibility = View.GONE + } + else -> { + } + } + } + + fun resumeToUpdateViewsByProgress( + fileName: String, + fileId: String, + mimeType: String?, + openWhenDownloaded: Boolean, + progressUi: ProgressUi + ) { + val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId) + + try { + for (workInfo in workers.get()) { + if (workInfo.state == WorkInfo.State.RUNNING || + workInfo.state == WorkInfo.State.ENQUEUED + ) { + progressUi.progressBar?.visibility = View.VISIBLE + WorkManager + .getInstance(context) + .getWorkInfoByIdLiveData(workInfo.id) + .observeForever { info: WorkInfo? -> + updateViewsByProgress( + fileName, + mimeType, + info!!, + progressUi, + openWhenDownloaded + ) + } + } + } + } catch (e: ExecutionException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } catch (e: InterruptedException) { + Log.e(TAG, "Error when checking if worker already exists", e) + } + } + + data class ProgressUi(val progressBar: ProgressBar?, val messageText: EmojiTextView?, val previewImage: ImageView) + + data class FileInfo(val fileId: String, val fileName: String, var fileSize: Long?) + + companion object { + private val TAG = FileViewerUtils::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ImageEmojiEditText.kt b/app/src/main/java/com/nextcloud/talk/utils/ImageEmojiEditText.kt new file mode 100644 index 0000000..d1ee3c4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ImageEmojiEditText.kt @@ -0,0 +1,62 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Dariusz Olszewski + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.Context +import android.net.Uri +import android.util.AttributeSet +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import androidx.core.view.inputmethod.EditorInfoCompat +import androidx.core.view.inputmethod.InputConnectionCompat +import androidx.emoji2.widget.EmojiEditText +import com.nextcloud.talk.utils.Mimetype.IMAGE_GIF +import com.nextcloud.talk.utils.Mimetype.IMAGE_JPEG +import com.nextcloud.talk.utils.Mimetype.IMAGE_PNG + +/* +Subclass of EmojiEditText with support for image keyboards - primarily for GIF handling. ;-) +Implementation based on this example: +https://developer.android.com/guide/topics/text/image-keyboard + */ +class ImageEmojiEditText : EmojiEditText { + + // Callback function to be called when the user selects an image, pass image Uri + lateinit var onCommitContentListener: ((Uri) -> Unit) + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { + val ic: InputConnection? = super.onCreateInputConnection(editorInfo) + + EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf(IMAGE_GIF, IMAGE_JPEG, IMAGE_PNG)) + + val callback = + InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, _ -> + + val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0 + if (lacksPermission) { + try { + inputContentInfo.requestPermission() + } catch (e: Exception) { + return@OnCommitContentListener false + } + } + + if (::onCommitContentListener.isInitialized) { + onCommitContentListener.invoke(inputContentInfo.contentUri) + return@OnCommitContentListener true + } else { + return@OnCommitContentListener false + } + } + + return InputConnectionCompat.createWrapper(ic!!, editorInfo, callback) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt new file mode 100644 index 0000000..aa4cc99 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.Context + +// TODO improve log handling. https://github.com/nextcloud/talk-android/issues/1376 +// writing logs to a file is temporarily disabled to avoid huge logfiles. + +object LoggingUtils { + fun writeLogEntryToFile(context: Context, logEntry: String) { + // val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.ROOT) + // val date = Date() + // val logEntryWithDateTime = dateFormat.format(date) + ": " + logEntry + "\n" + // + // try { + // val outputStream = context.openFileOutput( + // "nc_log.txt", + // Context.MODE_PRIVATE or Context.MODE_APPEND + // ) + // outputStream.write(logEntryWithDateTime.toByteArray()) + // outputStream.flush() + // outputStream.close() + // } catch (e: FileNotFoundException) { + // e.printStackTrace() + // } catch (e: IOException) { + // e.printStackTrace() + // } + } + + fun sendMailWithAttachment(context: Context) { + // val logFile = context.getFileStreamPath("nc_log.txt") + // val emailIntent = Intent(Intent.ACTION_SEND) + // val mailto = "android@nextcloud.com" + // emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(mailto)) + // emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Talk logs") + // emailIntent.type = TEXT_PLAIN + // emailIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + // val uri: Uri + // uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID, logFile) + // + // emailIntent.putExtra(Intent.EXTRA_STREAM, uri) + // emailIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + // context.startActivity(emailIntent) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/Mimetype.kt b/app/src/main/java/com/nextcloud/talk/utils/Mimetype.kt new file mode 100644 index 0000000..daf36d2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/Mimetype.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +object Mimetype { + const val IMAGE_PREFIX = "image/" + const val VIDEO_PREFIX = "video/" + const val TEXT_PREFIX = "text/" + const val AUDIO_PREFIX = "audio" + + const val IMAGE_PREFIX_GENERIC = "image/*" + const val VIDEO_PREFIX_GENERIC = "video/*" + const val TEXT_PREFIX_GENERIC = "text/*" + + const val FOLDER = "inode/directory" + + const val IMAGE_PNG = "image/png" + const val IMAGE_JPEG = "image/jpeg" + const val IMAGE_JPG = "image/jpg" + const val IMAGE_GIF = "image/gif" + const val IMAGE_HEIC = "image/heic" + + const val VIDEO_MP4 = "video/mp4" + const val VIDEO_QUICKTIME = "video/quicktime" + const val VIDEO_OGG = "video/ogg" + const val VIDEO_WEBM = "video/webm" + + const val TEXT_MARKDOWN = "text/markdown" + const val TEXT_PLAIN = "text/plain" + + const val AUDIO_MPEG = "audio/mpeg" + const val AUDIO_WAV = "audio/wav" + const val AUDIO_OGG = "audio/ogg" +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/MimetypeUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/MimetypeUtils.kt new file mode 100644 index 0000000..3c5fdb2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/MimetypeUtils.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +object MimetypeUtils { + fun isGif(mimetype: String): Boolean = Mimetype.IMAGE_GIF == mimetype + + fun isMarkdown(mimetype: String): Boolean = Mimetype.TEXT_MARKDOWN == mimetype + + fun isAudioOnly(mimetype: String): Boolean = mimetype.startsWith(Mimetype.AUDIO_PREFIX) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/NoSupportedApiException.kt b/app/src/main/java/com/nextcloud/talk/utils/NoSupportedApiException.kt new file mode 100644 index 0000000..12e05c9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/NoSupportedApiException.kt @@ -0,0 +1,9 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Joas Schilling + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +class NoSupportedApiException : RuntimeException("No supported API version found") diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt new file mode 100644 index 0000000..4023b80 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt @@ -0,0 +1,346 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.media.AudioAttributes +import android.net.Uri +import android.service.notification.StatusBarNotification +import android.text.TextUtils +import android.util.Log +import androidx.core.graphics.drawable.IconCompat +import androidx.core.net.toUri +import coil.executeBlocking +import coil.imageLoader +import coil.request.ImageRequest +import coil.transform.CircleCropTransformation +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.RingtoneSettings +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.preferences.AppPreferences +import java.io.IOException + +@Suppress("TooManyFunctions") +object NotificationUtils { + + const val TAG = "NotificationUtils" + + enum class NotificationChannels { + NOTIFICATION_CHANNEL_MESSAGES_V4, + NOTIFICATION_CHANNEL_CALLS_V4, + NOTIFICATION_CHANNEL_UPLOADS + } + + const val DEFAULT_CALL_RINGTONE_URI = + "android.resource://" + BuildConfig.APPLICATION_ID + "/raw/librem_by_feandesign_call" + const val DEFAULT_MESSAGE_RINGTONE_URI = + "android.resource://" + BuildConfig.APPLICATION_ID + "/raw/librem_by_feandesign_message" + + // RemoteInput key - used for replies sent directly from notification + const val KEY_DIRECT_REPLY = "key_direct_reply" + + // notification group keys + const val KEY_UPLOAD_GROUP = "com.nextcloud.talk.utils.KEY_UPLOAD_GROUP" + const val GROUP_SUMMARY_NOTIFICATION_ID = -1 + + private fun createNotificationChannel( + context: Context, + notificationChannel: Channel, + sound: Uri?, + audioAttributes: AudioAttributes? + ) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if ( + notificationManager.getNotificationChannel(notificationChannel.id) == null + ) { + val importance = if (notificationChannel.isImportant) { + NotificationManager.IMPORTANCE_HIGH + } else { + NotificationManager.IMPORTANCE_LOW + } + + val channel = NotificationChannel( + notificationChannel.id, + notificationChannel.name, + importance + ) + + channel.description = notificationChannel.description + channel.enableLights(true) + channel.lightColor = R.color.colorPrimary + channel.setSound(sound, audioAttributes) + channel.setBypassDnd(false) + + notificationManager.createNotificationChannel(channel) + } + } + + private fun createCallsNotificationChannel(context: Context, appPreferences: AppPreferences) { + val audioAttributes = + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST) + .build() + val soundUri = getCallRingtoneUri(context, appPreferences) + + createNotificationChannel( + context, + Channel( + NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name, + context.resources.getString(R.string.nc_notification_channel_calls), + context.resources.getString(R.string.nc_notification_channel_calls_description), + true + ), + soundUri, + audioAttributes + ) + } + + private fun createMessagesNotificationChannel(context: Context, appPreferences: AppPreferences) { + val audioAttributes = + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) + .build() + val soundUri = getMessageRingtoneUri(context, appPreferences) + + createNotificationChannel( + context, + Channel( + NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name, + context.resources.getString(R.string.nc_notification_channel_messages), + context.resources.getString(R.string.nc_notification_channel_messages_description), + true + ), + soundUri, + audioAttributes + ) + } + + private fun createUploadsNotificationChannel(context: Context) { + createNotificationChannel( + context, + Channel( + NotificationChannels.NOTIFICATION_CHANNEL_UPLOADS.name, + context.resources.getString(R.string.nc_notification_channel_uploads), + context.resources.getString(R.string.nc_notification_channel_uploads_description), + false + ), + null, + null + ) + } + + fun registerNotificationChannels(context: Context, appPreferences: AppPreferences) { + createCallsNotificationChannel(context, appPreferences) + createMessagesNotificationChannel(context, appPreferences) + createUploadsNotificationChannel(context) + } + + fun removeOldNotificationChannels(context: Context) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Current version does not use notification channel groups - delete all groups + for (channelGroup in notificationManager.notificationChannelGroups) { + notificationManager.deleteNotificationChannelGroup(channelGroup.id) + } + + val channelsToKeep = NotificationChannels.values().map { it.name } + + // Delete all notification channels created by previous versions + for (channel in notificationManager.notificationChannels) { + if (!channelsToKeep.contains(channel.id)) { + notificationManager.deleteNotificationChannel(channel.id) + } + } + } + + private fun getNotificationChannel(context: Context, channelId: String): NotificationChannel? { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + return notificationManager.getNotificationChannel(channelId) + } + + private inline fun scanNotifications( + context: Context?, + conversationUser: User, + callback: ( + notificationManager: NotificationManager, + statusBarNotification: StatusBarNotification, + notification: Notification + ) -> Unit + ) { + if (conversationUser.id == -1L || context == null) { + return + } + + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val statusBarNotifications = notificationManager.activeNotifications + var notification: Notification? + for (statusBarNotification in statusBarNotifications) { + notification = statusBarNotification.notification + + if ( + notification != null && + !notification.extras.isEmpty && + conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) + ) { + callback(notificationManager, statusBarNotification, notification) + } + } + } + + fun cancelAllNotificationsForAccount(context: Context?, conversationUser: User) { + scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, _ -> + notificationManager.cancel(statusBarNotification.id) + } + } + + fun cancelNotification(context: Context?, conversationUser: User, notificationId: Long?) { + scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification -> + if (notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)) { + notificationManager.cancel(statusBarNotification.id) + } + } + } + + fun findNotificationForRoom( + context: Context?, + conversationUser: User, + roomTokenOrId: String + ): StatusBarNotification? { + scanNotifications(context, conversationUser) { _, statusBarNotification, notification -> + if (roomTokenOrId == notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)) { + return statusBarNotification + } + } + return null + } + + fun cancelExistingNotificationsForRoom(context: Context?, conversationUser: User, roomTokenOrId: String) { + scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification -> + if (roomTokenOrId == notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN) && + !notification.extras.getBoolean(BundleKeys.KEY_NOTIFICATION_RESTRICT_DELETION) + ) { + notificationManager.cancel(statusBarNotification.id) + } + } + } + + fun isNotificationVisible(context: Context?, notificationId: Int): Boolean { + var isVisible = false + + val notificationManager = context!!.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notifications = notificationManager.activeNotifications + for (notification in notifications) { + if (notification.id == notificationId) { + isVisible = true + break + } + } + return isVisible + } + + fun isCallsNotificationChannelEnabled(context: Context): Boolean { + val channel = getNotificationChannel(context, NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name) + if (channel != null) { + return isNotificationChannelEnabled(channel) + } + return false + } + + fun isMessagesNotificationChannelEnabled(context: Context): Boolean { + val channel = getNotificationChannel(context, NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name) + if (channel != null) { + return isNotificationChannelEnabled(channel) + } + return false + } + + private fun isNotificationChannelEnabled(channel: NotificationChannel): Boolean = + channel.importance != NotificationManager.IMPORTANCE_NONE + + private fun getRingtoneUri( + context: Context, + ringtonePreferencesString: String?, + defaultRingtoneUri: String, + channelId: String + ): Uri? { + val channel = getNotificationChannel(context, channelId) + if (channel != null) { + return channel.sound + } + // Notification channel will not be available when starting the application for the first time. + // Ringtone uris are required to register the notification channels -> get uri from preferences. + + return if (TextUtils.isEmpty(ringtonePreferencesString)) { + defaultRingtoneUri.toUri() + } else { + try { + val ringtoneSettings = + LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java) + ringtoneSettings.ringtoneUri + } catch (exception: IOException) { + defaultRingtoneUri.toUri() + } + } + } + + fun getCallRingtoneUri(context: Context, appPreferences: AppPreferences): Uri? = + getRingtoneUri( + context, + appPreferences.callRingtoneUri, + DEFAULT_CALL_RINGTONE_URI, + NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name + ) + + fun getMessageRingtoneUri(context: Context, appPreferences: AppPreferences): Uri? = + getRingtoneUri( + context, + appPreferences.messageRingtoneUri, + DEFAULT_MESSAGE_RINGTONE_URI, + NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name + ) + + fun loadAvatarSync(avatarUrl: String, context: Context): IconCompat? { + var avatarIcon: IconCompat? = null + + val request = ImageRequest.Builder(context) + .data(avatarUrl) + .transformations(CircleCropTransformation()) + .placeholder(R.drawable.account_circle_96dp) + .target( + onSuccess = { result -> + val bitmap = (result as BitmapDrawable).bitmap + avatarIcon = IconCompat.createWithBitmap(bitmap) + }, + onError = { error -> + error?.let { + val bitmap = (error as BitmapDrawable).bitmap + avatarIcon = IconCompat.createWithBitmap(bitmap) + } + Log.w(TAG, "Can't load avatar for URL: $avatarUrl") + } + ) + .build() + + context.imageLoader.executeBlocking(request) + + return avatarIcon + } + + private data class Channel(val id: String, val name: String, val description: String, val isImportant: Boolean) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt b/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt new file mode 100644 index 0000000..a46bfa7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt @@ -0,0 +1,86 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability + +/** + * see https://nextcloud-talk.readthedocs.io/en/latest/constants/#attendee-permissions + */ +class ParticipantPermissions( + private val spreedCapabilities: SpreedCapability, + private val conversation: ConversationModel +) { + val isDefault = (conversation.permissions and DEFAULT) == DEFAULT + val isCustom = (conversation.permissions and CUSTOM) == CUSTOM + private val canStartCall = (conversation.permissions and START_CALL) == START_CALL + val canJoinCall = (conversation.permissions and JOIN_CALL) == JOIN_CALL + private val canIgnoreLobby = (conversation.permissions and CAN_IGNORE_LOBBY) == CAN_IGNORE_LOBBY + private val canPublishAudio = (conversation.permissions and PUBLISH_AUDIO) == PUBLISH_AUDIO + private val canPublishVideo = (conversation.permissions and PUBLISH_VIDEO) == PUBLISH_VIDEO + val canPublishScreen = (conversation.permissions and PUBLISH_SCREEN) == PUBLISH_SCREEN + private val hasChatPermission = (conversation.permissions and CHAT) == CHAT + + private fun hasConversationPermissions(): Boolean = + CapabilitiesUtil.hasSpreedFeatureCapability( + spreedCapabilities, + SpreedFeatures.CONVERSATION_PERMISSION + ) + + fun canIgnoreLobby(): Boolean { + if (hasConversationPermissions()) { + return canIgnoreLobby + } + + return false + } + + fun canStartCall(): Boolean = + if (hasConversationPermissions()) { + canStartCall + } else { + conversation.canStartCall + } + + fun canPublishAudio(): Boolean = + if (hasConversationPermissions()) { + canPublishAudio + } else { + true + } + + fun canPublishVideo(): Boolean = + if (hasConversationPermissions()) { + canPublishVideo + } else { + true + } + + fun hasChatPermission(): Boolean { + if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.CHAT_PERMISSION)) { + return hasChatPermission + } + // if capability is not available then the spreed version doesn't support to restrict this + return true + } + + companion object { + + val TAG = ParticipantPermissions::class.simpleName + const val DEFAULT = 0 + const val CUSTOM = 1 + const val START_CALL = 2 + const val JOIN_CALL = 4 + const val CAN_IGNORE_LOBBY = 8 + const val PUBLISH_AUDIO = 16 + const val PUBLISH_VIDEO = 32 + const val PUBLISH_SCREEN = 64 + const val CHAT = 128 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/PickImage.kt b/app/src/main/java/com/nextcloud/talk/utils/PickImage.kt new file mode 100644 index 0000000..cfe44fa --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/PickImage.kt @@ -0,0 +1,179 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Parneet Singh + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.utils + +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.activity.result.ActivityResultLauncher +import autodagger.AutoInjector +import com.github.dhaval2404.imagepicker.ImagePicker +import com.github.dhaval2404.imagepicker.constant.ImageProvider +import com.nextcloud.talk.activities.TakePhotoActivity +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PickImage(private val activity: Activity, private var currentUser: User?) { + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var permissionUtil: PlatformPermissionUtil + + init { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + } + + fun selectLocal(startImagePickerForResult: ActivityResultLauncher) { + ImagePicker.Companion.with(activity) + .provider(ImageProvider.GALLERY) + .crop() + .cropSquare() + .compress(MAX_SIZE) + .maxResultSize(MAX_SIZE, MAX_SIZE) + .createIntent { intent -> + startImagePickerForResult.launch(intent) + } + } + + private fun selectLocal(startImagePickerForResult: ActivityResultLauncher, file: File) { + ImagePicker.Companion.with(activity) + .provider(ImageProvider.URI) + .crop() + .cropSquare() + .compress(MAX_SIZE) + .maxResultSize(MAX_SIZE, MAX_SIZE) + .setUri(Uri.fromFile(file)) + .createIntent { intent -> + startImagePickerForResult.launch(intent) + } + } + + fun selectRemote(startSelectRemoteFilesIntentForResult: ActivityResultLauncher) { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_MIME_TYPE_FILTER, Mimetype.IMAGE_PREFIX) + + val avatarIntent = Intent(activity, RemoteFileBrowserActivity::class.java) + avatarIntent.putExtras(bundle) + startSelectRemoteFilesIntentForResult.launch(avatarIntent) + } + + fun takePicture(startTakePictureIntentForResult: ActivityResultLauncher) { + if (permissionUtil.isCameraPermissionGranted()) { + startTakePictureIntentForResult.launch(TakePhotoActivity.createIntent(activity)) + } else { + activity.requestPermissions( + arrayOf(android.Manifest.permission.CAMERA), + REQUEST_PERMISSION_CAMERA + ) + } + } + + private fun handleAvatar(startImagePickerForResult: ActivityResultLauncher, remotePath: String?) { + val uri = currentUser!!.baseUrl + "/index.php/apps/files/api/v1/thumbnail/512/512/" + + Uri.encode(remotePath, "/") + val downloadCall = ncApi.downloadResizedImage( + ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token), + uri + ) + downloadCall.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + saveBitmapAndPassToImagePicker( + startImagePickerForResult, + BitmapFactory.decodeStream(response.body()!!.byteStream()) + ) + } + + override fun onFailure(call: Call, t: Throwable) { + // unused atm + } + }) + } + + // only possible with API26 + private fun saveBitmapAndPassToImagePicker( + startImagePickerForResult: ActivityResultLauncher, + bitmap: Bitmap + ) { + val file: File = saveBitmapToTempFile(bitmap) ?: return + selectLocal(startImagePickerForResult, file) + } + + private fun saveBitmapToTempFile(bitmap: Bitmap): File? { + try { + val file = createTempFileForAvatar() + try { + FileOutputStream(file).use { out -> + bitmap.compress(Bitmap.CompressFormat.PNG, FULL_QUALITY, out) + } + return file + } catch (e: IOException) { + Log.e(TAG, "Error compressing bitmap", e) + } + } catch (e: IOException) { + Log.e(TAG, "Error creating temporary avatar image", e) + } + return null + } + + private fun createTempFileForAvatar(): File { + FileUtils.removeTempCacheFile( + activity, + AVATAR_PATH + ) + return FileUtils.getTempCacheFile( + activity, + AVATAR_PATH + ) + } + + fun onImagePickerResult(data: Intent?, handleImage: (uri: Uri) -> Unit) { + val uri: Uri = data?.data!! + handleImage(uri) + } + + fun onSelectRemoteFilesResult(startImagePickerForResult: ActivityResultLauncher, data: Intent?) { + val pathList = data?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS) + if (pathList?.size!! >= 1) { + handleAvatar(startImagePickerForResult, pathList[0]) + } + } + + fun onTakePictureResult(startImagePickerForResult: ActivityResultLauncher, data: Intent?) { + data?.data?.path?.let { + selectLocal(startImagePickerForResult, File(it)) + } + } + + companion object { + private const val TAG: String = "PickImage" + private const val MAX_SIZE: Int = 1024 + private const val AVATAR_PATH = "photos/avatar.png" + private const val FULL_QUALITY: Int = 100 + const val REQUEST_PERMISSION_CAMERA: Int = 1 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt new file mode 100644 index 0000000..95e59b2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt @@ -0,0 +1,412 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.Context +import android.util.Base64 +import android.util.Log +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.events.EventStatus +import com.nextcloud.talk.models.SignatureVerification +import com.nextcloud.talk.models.json.push.PushConfigurationState +import com.nextcloud.talk.models.json.push.PushRegistrationOverall +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.UserIdUtils.getIdForUser +import com.nextcloud.talk.utils.preferences.AppPreferences +import io.reactivex.Observer +import io.reactivex.SingleObserver +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import org.greenrobot.eventbus.EventBus +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.security.InvalidKeyException +import java.security.Key +import java.security.KeyFactory +import java.security.KeyPairGenerator +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.PublicKey +import java.security.Signature +import java.security.SignatureException +import java.security.spec.InvalidKeySpecException +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec +import java.util.Locale +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PushUtils { + @JvmField + @Inject + var userManager: UserManager? = null + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var arbitraryStorageManager: ArbitraryStorageManager + + @JvmField + @Inject + var eventBus: EventBus? = null + private val publicKeyFile: File + private val privateKeyFile: File + private val proxyServer: String + + init { + sharedApplication!!.componentApplication.inject(this) + val keyPath = sharedApplication!! + .getDir("PushKeystore", Context.MODE_PRIVATE) + .absolutePath + publicKeyFile = File(keyPath, "push_key.pub") + privateKeyFile = File(keyPath, "push_key.priv") + proxyServer = sharedApplication!! + .resources.getString(R.string.nc_push_server_url) + } + + fun verifySignature(signatureBytes: ByteArray?, subjectBytes: ByteArray?): SignatureVerification { + val signatureVerification = SignatureVerification() + signatureVerification.signatureValid = false + val users = userManager!!.users.blockingGet() + try { + val signature = Signature.getInstance("SHA512withRSA") + if (users != null && users.size > 0) { + var publicKey: PublicKey? + for (user in users) { + if (user.pushConfigurationState != null) { + publicKey = readKeyFromString( + true, + user.pushConfigurationState!!.userPublicKey + ) as PublicKey? + signature.initVerify(publicKey) + signature.update(subjectBytes) + if (signature.verify(signatureBytes)) { + signatureVerification.signatureValid = true + signatureVerification.user = user + return signatureVerification + } + } + } + } + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "No such algorithm") + } catch (e: InvalidKeyException) { + Log.d(TAG, "Invalid key while trying to verify") + } catch (e: SignatureException) { + Log.d(TAG, "Signature exception while trying to verify") + } + return signatureVerification + } + + private fun saveKeyToFile(key: Key, path: String): Int { + val encoded = key.encoded + try { + return if (!File(path).exists() && !File(path).createNewFile()) { + -1 + } else { + FileOutputStream(path).use { keyFileOutputStream -> + keyFileOutputStream.write(encoded) + 0 + } + } + } catch (e: FileNotFoundException) { + Log.d(TAG, "Failed to save key to file") + } catch (e: IOException) { + Log.d(TAG, "Failed to save key to file via IOException") + } + return -1 + } + + private fun generateSHA512Hash(pushToken: String): String { + var messageDigest: MessageDigest? = null + try { + messageDigest = MessageDigest.getInstance("SHA-512") + messageDigest.update(pushToken.toByteArray()) + return bytesToHex(messageDigest.digest()) + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "SHA-512 algorithm not supported") + } + return "" + } + + private fun bytesToHex(bytes: ByteArray): String { + val result = StringBuilder() + for (individualByte in bytes) { + result.append( + ((individualByte.toInt() and BYTES_TO_HEX_SUFFIX) + BYTES_TO_HEX_SUFFIX_SUFFIX) + .toString(BYTES_TO_HEX_RADIX) + .substring(1) + ) + } + return result.toString() + } + + fun generateRsa2048KeyPair(): Int { + if (!publicKeyFile.exists() && !privateKeyFile.exists()) { + var keyGen: KeyPairGenerator? = null + try { + keyGen = KeyPairGenerator.getInstance("RSA") + keyGen.initialize(RSA_KEY_SIZE) + val pair = keyGen.generateKeyPair() + val statusPrivate = saveKeyToFile(pair.private, privateKeyFile.absolutePath) + val statusPublic = saveKeyToFile(pair.public, publicKeyFile.absolutePath) + return if (statusPrivate == 0 && statusPublic == 0) { + // all went well + RETURN_CODE_KEY_GENERATION_SUCCESSFUL + } else { + RETURN_CODE_KEY_GENERATION_FAILED + } + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "RSA algorithm not supported") + } + } else { + // We already have the key + return RETURN_CODE_KEY_ALREADY_EXISTS + } + + // we failed to generate the key + return RETURN_CODE_KEY_GENERATION_FAILED + } + + fun pushRegistrationToServer(ncApi: NcApi) { + val pushToken = appPreferences.pushToken + + if (pushToken.isNotEmpty()) { + Log.d(TAG, "pushRegistrationToServer will be done with pushToken: $pushToken") + val pushTokenHash = generateSHA512Hash(pushToken).lowercase(Locale.getDefault()) + val devicePublicKey = readKeyFromFile(true) as PublicKey? + if (devicePublicKey != null) { + val devicePublicKeyBytes = Base64.encode(devicePublicKey.encoded, Base64.NO_WRAP) + var devicePublicKeyBase64 = String(devicePublicKeyBytes) + devicePublicKeyBase64 = devicePublicKeyBase64.replace("(.{64})".toRegex(), "$1\n") + devicePublicKeyBase64 = "-----BEGIN PUBLIC KEY-----\n$devicePublicKeyBase64\n-----END PUBLIC KEY-----" + + val users = userManager!!.users.blockingGet() + for (user in users) { + if (!user.scheduledForDeletion) { + val nextcloudRegisterPushMap: MutableMap = HashMap() + nextcloudRegisterPushMap["format"] = "json" + nextcloudRegisterPushMap["pushTokenHash"] = pushTokenHash + nextcloudRegisterPushMap["devicePublicKey"] = devicePublicKeyBase64 + nextcloudRegisterPushMap["proxyServer"] = proxyServer + registerDeviceWithNextcloud(ncApi, nextcloudRegisterPushMap, pushToken, user) + } + } + } + } else { + Log.e(TAG, "push token was empty when trying to register at server") + } + } + + private fun registerDeviceWithNextcloud( + ncApi: NcApi, + nextcloudRegisterPushMap: Map, + token: String, + user: User + ) { + val credentials = ApiUtils.getCredentials(user.username, user.token) + ncApi.registerDeviceForNotificationsWithNextcloud( + credentials, + ApiUtils.getUrlNextcloudPush(user.baseUrl!!), + nextcloudRegisterPushMap + ) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(pushRegistrationOverall: PushRegistrationOverall) { + arbitraryStorageManager.storeStorageSetting( + getIdForUser(user), + LATEST_PUSH_REGISTRATION_AT_SERVER, + System.currentTimeMillis().toString(), + "" + ) + + Log.d(TAG, "pushTokenHash successfully registered at nextcloud server.") + val proxyMap: MutableMap = HashMap() + proxyMap["pushToken"] = token + proxyMap["deviceIdentifier"] = pushRegistrationOverall.ocs!!.data!!.deviceIdentifier + proxyMap["deviceIdentifierSignature"] = pushRegistrationOverall.ocs!!.data!!.signature + proxyMap["userPublicKey"] = pushRegistrationOverall.ocs!!.data!!.publicKey + registerDeviceWithPushProxy(ncApi, proxyMap, user) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to register device with nextcloud", e) + eventBus!!.post(EventStatus(user.id!!, EventStatus.EventType.PUSH_REGISTRATION, false)) + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun registerDeviceWithPushProxy(ncApi: NcApi, proxyMap: Map, user: User) { + ncApi.registerDeviceForNotificationsWithPushProxy(ApiUtils.getUrlPushProxy(), proxyMap) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(t: Unit) { + try { + arbitraryStorageManager.storeStorageSetting( + getIdForUser(user), + LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY, + System.currentTimeMillis().toString(), + "" + ) + + Log.d(TAG, "pushToken successfully registered at pushproxy.") + updatePushStateForUser(proxyMap, user) + } catch (e: IOException) { + Log.e(TAG, "IOException while updating user", e) + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to register device with pushproxy", e) + eventBus!!.post(EventStatus(user.id!!, EventStatus.EventType.PUSH_REGISTRATION, false)) + } + + override fun onComplete() { + // unused atm + } + }) + } + + @Throws(IOException::class) + private fun updatePushStateForUser(proxyMap: Map, user: User) { + val pushConfigurationState = PushConfigurationState() + pushConfigurationState.pushToken = proxyMap["pushToken"] + pushConfigurationState.deviceIdentifier = proxyMap["deviceIdentifier"] + pushConfigurationState.deviceIdentifierSignature = proxyMap["deviceIdentifierSignature"] + pushConfigurationState.userPublicKey = proxyMap["userPublicKey"] + pushConfigurationState.usesRegularPass = java.lang.Boolean.FALSE + if (user.id != null) { + userManager!!.updatePushState(user.id!!, pushConfigurationState).subscribe(object : SingleObserver { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onSuccess(integer: Int) { + eventBus!!.post( + EventStatus( + getIdForUser(user), + EventStatus.EventType.PUSH_REGISTRATION, + true + ) + ) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "update push state for user failed", e) + eventBus!!.post( + EventStatus( + getIdForUser(user), + EventStatus.EventType.PUSH_REGISTRATION, + false + ) + ) + } + }) + } else { + Log.e(TAG, "failed to update updatePushStateForUser. user.getId() was null") + } + } + + private fun readKeyFromString(readPublicKey: Boolean, keyString: String?): Key? { + var keyString = keyString + keyString = if (readPublicKey) { + keyString!!.replace("\\n".toRegex(), "").replace( + "-----BEGIN PUBLIC KEY-----", + "" + ).replace("-----END PUBLIC KEY-----", "") + } else { + keyString!!.replace("\\n".toRegex(), "").replace( + "-----BEGIN PRIVATE KEY-----", + "" + ).replace("-----END PRIVATE KEY-----", "") + } + var keyFactory: KeyFactory? = null + try { + keyFactory = KeyFactory.getInstance("RSA") + return if (readPublicKey) { + val keySpec = X509EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT)) + keyFactory.generatePublic(keySpec) + } else { + val keySpec = PKCS8EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT)) + keyFactory.generatePrivate(keySpec) + } + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "No such algorithm while reading key from string") + } catch (e: InvalidKeySpecException) { + Log.d(TAG, "Invalid key spec while reading key from string") + } + return null + } + + fun readKeyFromFile(readPublicKey: Boolean): Key? { + val path: String + path = if (readPublicKey) { + publicKeyFile.absolutePath + } else { + privateKeyFile.absolutePath + } + try { + FileInputStream(path).use { fileInputStream -> + val bytes = ByteArray(fileInputStream.available()) + fileInputStream.read(bytes) + val keyFactory = KeyFactory.getInstance("RSA") + return if (readPublicKey) { + val keySpec = X509EncodedKeySpec(bytes) + keyFactory.generatePublic(keySpec) + } else { + val keySpec = PKCS8EncodedKeySpec(bytes) + keyFactory.generatePrivate(keySpec) + } + } + } catch (e: FileNotFoundException) { + Log.d(TAG, "Failed to find path while reading the Key") + } catch (e: IOException) { + Log.d(TAG, "IOException while reading the key") + } catch (e: InvalidKeySpecException) { + Log.d(TAG, "InvalidKeySpecException while reading the key") + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "RSA algorithm not supported") + } + return null + } + + companion object { + private const val TAG = "PushUtils" + private const val RSA_KEY_SIZE: Int = 2048 + private const val RETURN_CODE_KEY_GENERATION_SUCCESSFUL: Int = 0 + private const val RETURN_CODE_KEY_ALREADY_EXISTS: Int = -1 + private const val RETURN_CODE_KEY_GENERATION_FAILED: Int = -2 + private const val BYTES_TO_HEX_RADIX: Int = 16 + private const val BYTES_TO_HEX_SUFFIX = 0xff + private const val BYTES_TO_HEX_SUFFIX_SUFFIX = 0x100 + const val LATEST_PUSH_REGISTRATION_AT_SERVER: String = "LATEST_PUSH_REGISTRATION_AT_SERVER" + const val LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY: String = "LATEST_PUSH_REGISTRATION_AT_PUSH_PROXY" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ReceiverFlag.kt b/app/src/main/java/com/nextcloud/talk/utils/ReceiverFlag.kt new file mode 100644 index 0000000..58df1ac --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ReceiverFlag.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.Context +import android.os.Build + +enum class ReceiverFlag(val value: Int) { + Exported( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Context.RECEIVER_EXPORTED + } else { + 0 + } + ), + NotExported( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Context.RECEIVER_NOT_EXPORTED + } else { + 0 + } + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/RemoteFileUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/RemoteFileUtils.kt new file mode 100644 index 0000000..1819e84 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/RemoteFileUtils.kt @@ -0,0 +1,72 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2020 Chris Narkiewicz + * SPDX-FileCopyrightText: 2016 ownCloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +object RemoteFileUtils { + private val TAG = RemoteFileUtils::class.java.simpleName + + fun getNewPathIfFileExists(ncApi: NcApi, currentUser: User, remotePath: String): String { + var finalPath = remotePath + val fileExists = doesFileExist( + ncApi, + currentUser, + remotePath + ).blockingFirst() + + if (fileExists) { + finalPath = getFileNameWithoutCollision( + ncApi, + currentUser, + remotePath + ) + } + return finalPath + } + + private fun doesFileExist(ncApi: NcApi, currentUser: User, remotePath: String): Observable = + ncApi.checkIfFileExists( + ApiUtils.getCredentials(currentUser.username, currentUser.token), + ApiUtils.getUrlForFileUpload( + currentUser.baseUrl!!, + currentUser.userId!!, + remotePath + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()).map { response -> + response.isSuccessful + } + + private fun getFileNameWithoutCollision(ncApi: NcApi, currentUser: User, remotePath: String): String { + val extPos = remotePath.lastIndexOf('.') + var suffix: String + var extension = "" + var remotePathWithoutExtension = "" + if (extPos >= 0) { + extension = remotePath.substring(extPos + 1) + remotePathWithoutExtension = remotePath.substring(0, extPos) + } + var count = 2 + var exists: Boolean + var newPath: String + do { + suffix = " ($count)" + newPath = if (extPos >= 0) "$remotePathWithoutExtension$suffix.$extension" else remotePath + suffix + exists = doesFileExist(ncApi, currentUser, newPath).blockingFirst() + count++ + } while (exists) + return newPath + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/SecurityUtils.java b/app/src/main/java/com/nextcloud/talk/utils/SecurityUtils.java new file mode 100644 index 0000000..bd58a7c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/SecurityUtils.java @@ -0,0 +1,116 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils; + +import android.content.res.Resources; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProperties; +import android.security.keystore.UserNotAuthenticatedException; +import android.util.Log; + +import com.nextcloud.talk.R; +import com.nextcloud.talk.application.NextcloudTalkApplication; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.List; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +import androidx.biometric.BiometricPrompt; + +public class SecurityUtils { + private static final String TAG = "SecurityUtils"; + private static final String CREDENTIALS_KEY = "KEY_CREDENTIALS"; + private static final byte[] SECRET_BYTE_ARRAY = new byte[]{1, 2, 3, 4, 5, 6}; + + private static BiometricPrompt.CryptoObject cryptoObject; + + public static boolean checkIfWeAreAuthenticated(String screenLockTimeout) { + try { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + SecretKey secretKey = (SecretKey) keyStore.getKey(CREDENTIALS_KEY, null); + Cipher cipher = + Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE); + + // Try encrypting something, it will only work if the user authenticated within + // the last AUTHENTICATION_DURATION_SECONDS seconds. + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + cipher.doFinal(SECRET_BYTE_ARRAY); + + cryptoObject = new BiometricPrompt.CryptoObject(cipher); + // If the user has recently authenticated, we will reach here + return true; + } catch (UserNotAuthenticatedException e) { + // User is not authenticated, let's authenticate with device credentials. + return false; + } catch (KeyPermanentlyInvalidatedException e) { + // This happens if the lock screen has been disabled or reset after the key was + // generated. + // Shouldn't really happen because we regenerate the key every time an activity + // is created, but oh well + // Create key, and attempt again + createKey(screenLockTimeout); + return false; + } catch (BadPaddingException | IllegalBlockSizeException | KeyStoreException | + CertificateException | UnrecoverableKeyException | IOException + | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) { + return false; + } + } + + public static BiometricPrompt.CryptoObject getCryptoObject() { + return cryptoObject; + } + + public static void createKey(String validity) { + try { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + KeyGenerator keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); + + keyGenerator.init(new KeyGenParameterSpec.Builder(CREDENTIALS_KEY, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setRandomizedEncryptionRequired(true) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setUserAuthenticationRequired(true) + .setUserAuthenticationValidityDurationSeconds(getIntegerFromStringTimeout(validity)) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build()); + + keyGenerator.generateKey(); + } catch (NoSuchAlgorithmException | NoSuchProviderException + | InvalidAlgorithmParameterException | KeyStoreException + | CertificateException | IOException e) { + Log.e(TAG, "Failed to create a symmetric key"); + } + } + + private static int getIntegerFromStringTimeout(String validity) { + Resources resources = NextcloudTalkApplication.Companion.getSharedApplication().getResources(); + List entryValues = Arrays.asList(resources.getStringArray(R.array.screen_lock_timeout_entry_values)); + int[] entryIntValues = resources.getIntArray(R.array.screen_lock_timeout_entry_int_values); + int indexOfValidity = entryValues.indexOf(validity); + return entryIntValues[indexOfValidity]; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ShareUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/ShareUtils.kt new file mode 100644 index 0000000..1031508 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ShareUtils.kt @@ -0,0 +1,58 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import androidx.core.net.toUri +import com.nextcloud.talk.R + +object ShareUtils { + + @SuppressLint("StringFormatMatches") + fun shareConversationLink( + context: Activity, + baseUrl: String?, + roomToken: String?, + conversationName: String?, + canGeneratePrettyURL: Boolean + ) { + if (baseUrl.isNullOrBlank() || roomToken.isNullOrBlank() || conversationName.isNullOrBlank()) { + return + } + + val uriBuilder = baseUrl.toUri() + .buildUpon() + + if (!canGeneratePrettyURL) { + uriBuilder.appendPath("index.php") + } + + uriBuilder.appendPath("call") + uriBuilder.appendPath(roomToken) + + val uriToShareConversation = uriBuilder.build() + + val shareConversationLink = String.format( + context.getString( + R.string.share_link_to_conversation, + conversationName, + uriToShareConversation.toString() + ) + ) + + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, shareConversationLink) + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, context.getString(R.string.nc_share_link)) + context.startActivity(shareIntent) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/SyncAdapter.java b/app/src/main/java/com/nextcloud/talk/utils/SyncAdapter.java new file mode 100644 index 0000000..5358ddf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/SyncAdapter.java @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils; + +import android.accounts.Account; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.SyncResult; +import android.os.Bundle; +import android.util.Log; + +class SyncAdapter extends AbstractThreadedSyncAdapter { + + public SyncAdapter(Context context, boolean autoInitialize) { + super(context, autoInitialize); + Log.i("SyncAdapter", "Sync adapter created"); + } + + @Override + public void onPerformSync(Account account, Bundle extras, String authority, + ContentProviderClient provider, SyncResult syncResult) { + Log.i("SyncAdapter", "Sync adapter called"); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/SyncService.java b/app/src/main/java/com/nextcloud/talk/utils/SyncService.java new file mode 100644 index 0000000..49ee026 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/SyncService.java @@ -0,0 +1,36 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils; + + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; + +public class SyncService extends Service { + + private static final Object sSyncAdapterLock = new Object(); + + private static SyncAdapter sSyncAdapter = null; + + @Override + public void onCreate() { + Log.i("SyncService", "Sync service created"); + synchronized (sSyncAdapterLock) { + if (sSyncAdapter == null) { + sSyncAdapter = new SyncAdapter(getApplicationContext(), true); + } + } + } + + @Override + public IBinder onBind(Intent intent) { + Log.i("SyncService", "Sync service binded"); + return sSyncAdapter.getSyncAdapterBinder(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/TextDrawable.kt b/app/src/main/java/com/nextcloud/talk/utils/TextDrawable.kt new file mode 100644 index 0000000..ce44f31 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/TextDrawable.kt @@ -0,0 +1,59 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.utils + +import android.content.Context +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.drawable.Drawable +import com.nextcloud.talk.R + +class TextDrawable(val context: Context, private var text: String) : Drawable() { + private val paint = Paint() + private val bounds: Rect + + init { + paint.color = context.getColor(R.color.textColorOnPrimaryBackground) + paint.isAntiAlias = true + paint.textSize = TEXT_SIZE + bounds = Rect() + } + + override fun draw(canvas: Canvas) { + if (text.isNotEmpty()) { + paint.getTextBounds( + text, + 0, + text.length, + bounds + ) + val x: Int = (getBounds().width() - bounds.width()) / 2 + val y: Int = ((getBounds().height() + bounds.height()) / 2) + Y_OFFSET + canvas.drawText(text, x.toFloat(), y.toFloat(), paint) + } + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.setColorFilter(colorFilter) + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + @Deprecated("Deprecated in Java", ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat")) + override fun getOpacity(): Int = PixelFormat.OPAQUE + + companion object { + private const val Y_OFFSET = 5 + private const val TEXT_SIZE = 50f + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/TextMatchers.java b/app/src/main/java/com/nextcloud/talk/utils/TextMatchers.java new file mode 100644 index 0000000..58aabdb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/TextMatchers.java @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-FileCopyrightText: 2017 Keval Patel + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Partly based on https://github.com/kevalpatel2106/EmoticonGIFKeyboard/blob/master/emoticongifkeyboard/src/main/java/com/kevalpatel2106/emoticongifkeyboard/internal/emoticon/EmoticonUtils.java + */ +package com.nextcloud.talk.utils; + +import com.vanniktech.emoji.EmojiInformation; +import com.vanniktech.emoji.Emojis; + +import androidx.annotation.Nullable; + +public final class TextMatchers { + + public static boolean isMessageWithSingleEmoticonOnly(@Nullable final String text) { + final EmojiInformation emojiInformation = Emojis.emojiInformation(text); + return (emojiInformation.isOnlyEmojis && emojiInformation.emojis.size() == 1); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt new file mode 100644 index 0000000..2ef5726 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/UriUtils.kt @@ -0,0 +1,65 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import androidx.core.net.toUri + +class UriUtils { + companion object { + fun hasHttpProtocolPrefixed(uri: String): Boolean = uri.startsWith("http://") || uri.startsWith("https://") + + fun extractInstanceInternalFileFileId(url: String): String { + // https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41 + return url.toUri().getQueryParameter("fileid").toString() + } + + fun isInstanceInternalFileShareUrl(baseUrl: String, url: String): Boolean { + // https://cloud.nextcloud.com/f/41 + // https://cloud.nextcloud.com/index.php/f/41 + return (url.startsWith("$baseUrl/f/") || url.startsWith("$baseUrl/index.php/f/")) && + Regex(".*/f/\\d*").matches(url) + } + + fun isInstanceInternalTalkUrl(baseUrl: String, url: String): Boolean { + // https://cloud.nextcloud.com/call/123456789 + return (url.startsWith("$baseUrl/call/") || url.startsWith("$baseUrl/index.php/call/")) && + Regex(".*/call/\\d*").matches(url) + } + + fun extractInstanceInternalFileShareFileId(url: String): String { + // https://cloud.nextcloud.com/f/41 + return url.toUri().lastPathSegment ?: "" + } + + fun extractRoomTokenFromTalkUrl(url: String): String { + // https://cloud.nextcloud.com/call/123456789 + return url.toUri().lastPathSegment ?: "" + } + + fun isInstanceInternalFileUrl(baseUrl: String, url: String): Boolean { + // https://cloud.nextcloud.com/apps/files/?dir=/Engineering&fileid=41 + return ( + url.startsWith("$baseUrl/apps/files/") || + url.startsWith("$baseUrl/index.php/apps/files/") + ) && + url.toUri().queryParameterNames.contains("fileid") && + Regex(""".*fileid=\d*""").matches(url) + } + + fun isInstanceInternalFileUrlNew(baseUrl: String, url: String): Boolean { + // https://cloud.nextcloud.com/apps/files/files/41?dir=/ + return url.startsWith("$baseUrl/apps/files/files/") || + url.startsWith("$baseUrl/index.php/apps/files/files/") + } + + fun extractInstanceInternalFileFileIdNew(url: String): String { + // https://cloud.nextcloud.com/apps/files/files/41?dir=/ + return url.toUri().lastPathSegment ?: "" + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/UserIdUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/UserIdUtils.kt new file mode 100644 index 0000000..cc33a10 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/UserIdUtils.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.data.user.model.User + +object UserIdUtils { + const val NO_ID: Long = -1 + + fun getIdForUser(user: User?): Long = + if (user?.id != null) { + user.id!! + } else { + NO_ID + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/VibrationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/VibrationUtils.kt new file mode 100644 index 0000000..f73a926 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/VibrationUtils.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.content.Context +import android.os.VibrationEffect +import android.os.Vibrator + +object VibrationUtils { + const val SHORT_VIBRATE: Long = 100 + + fun vibrateShort(context: Context) { + val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + vibrator.vibrate(VibrationEffect.createOneShot(SHORT_VIBRATE, VibrationEffect.DEFAULT_AMPLITUDE)) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/animations/PulseAnimation.java b/app/src/main/java/com/nextcloud/talk/utils/animations/PulseAnimation.java new file mode 100644 index 0000000..2bbb3fb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/animations/PulseAnimation.java @@ -0,0 +1,72 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Original code taken from https://github.com/thunderrise/android-TNRAnimationHelper under MIT licence + */ +package com.nextcloud.talk.utils.animations; + +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.view.View; +import androidx.annotation.NonNull; + +public class PulseAnimation { + + public static final int RESTART = 1; + public static final int REVERSE = 2; + public static final int INFINITE = -1; + private ObjectAnimator scaleDown; + private int duration = 310; + private View view; + private int repeatMode = ValueAnimator.RESTART; + private int repeatCount = INFINITE; + + public static PulseAnimation create() { + return new PulseAnimation(); + } + + public PulseAnimation with(@NonNull View view) { + this.view = view; + return this; + } + + public void start() { + + if (view == null) throw new NullPointerException("View cant be null!"); + + scaleDown = ObjectAnimator.ofPropertyValuesHolder(view, PropertyValuesHolder.ofFloat("scaleX", 1.2f), PropertyValuesHolder.ofFloat("scaleY", 1.2f)); + scaleDown.setDuration(duration); + scaleDown.setRepeatMode(repeatMode); + scaleDown.setRepeatCount(repeatCount); + scaleDown.setAutoCancel(true); + scaleDown.start(); + } + + public void stop() { + if (scaleDown != null && view != null) { + scaleDown.end(); + scaleDown.cancel(); + view.setScaleX(1.0f); + view.setScaleY(1.0f); + } + } + + public PulseAnimation setDuration(int duration) { + this.duration = duration; + return this; + } + + public PulseAnimation setRepeatMode(int repeatMode) { + this.repeatMode = repeatMode; + return this; + } + + public PulseAnimation setRepeatCount(int repeatCount) { + this.repeatCount = repeatCount; + return this; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/animations/ViewHidingBehaviourAnimation.java b/app/src/main/java/com/nextcloud/talk/utils/animations/ViewHidingBehaviourAnimation.java new file mode 100644 index 0000000..91c2e69 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/animations/ViewHidingBehaviourAnimation.java @@ -0,0 +1,61 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2018 Mario Danic + * SPDX-FileCopyrightText: 2016 Srijith Narayanan + * SPDX-License-Identifier: GPL-3.0-or-later + * + * The original code is Copyright 2016 Srijith Narayanan under MIT licence + * https://github.com/sjthn/BottomNavigationViewBehavior/blob/9558104a16a1276bd8a73fba6736d88cd25b5488/app/src/main/java/com/example/srijith/bottomnavigationviewbehavior/BottomNavigationViewBehavior.java + */ +package com.nextcloud.talk.utils.animations; + +import android.view.View; +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.ViewCompat; + +public class ViewHidingBehaviourAnimation extends CoordinatorLayout.Behavior { + + private int height; + private boolean slidingDown = false; + + @Override + public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) { + height = child.getHeight(); + return super.onLayoutChild(parent, child, layoutDirection); + } + + @Override + public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View + target, int nestedScrollAxes) { + return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL; + } + + @Override + public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int + dyConsumed, int dxUnconsumed, int dyUnconsumed) { + if (dyConsumed > 0) { + slideDown(child); + } else if (dyConsumed < 0) { + slideUp(child); + } + } + + private void slideUp(View child) { + if (slidingDown) { + slidingDown = false; + child.clearAnimation(); + child.animate().translationY(0).setDuration(200); + } + } + + private void slideDown(View child) { + if (!slidingDown) { + slidingDown = true; + child.clearAnimation(); + child.animate().translationY(height).setDuration(200); + } + } + +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt new file mode 100644 index 0000000..7d6d528 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -0,0 +1,87 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.bundle + +object BundleKeys { + const val KEY_SELECTED_USERS = "KEY_SELECTED_USERS" + const val KEY_SELECTED_GROUPS = "KEY_SELECTED_GROUPS" + const val KEY_SELECTED_CIRCLES = "KEY_SELECTED_CIRCLES" + const val KEY_SELECTED_EMAILS = "KEY_SELECTED_EMAILS" + const val KEY_USERNAME = "KEY_USERNAME" + const val KEY_TOKEN = "KEY_TOKEN" + const val KEY_TRANSLATE_MESSAGE = "KEY_TRANSLATE_MESSAGE" + const val KEY_BASE_URL = "KEY_BASE_URL" + const val KEY_IS_ACCOUNT_IMPORT = "KEY_IS_ACCOUNT_IMPORT" + const val KEY_ORIGINAL_PROTOCOL = "KEY_ORIGINAL_PROTOCOL" + const val KEY_OPERATION_CODE = "KEY_OPERATION_CODE" + const val KEY_APP_ITEM_PACKAGE_NAME = "KEY_APP_ITEM_PACKAGE_NAME" + const val KEY_APP_ITEM_NAME = "KEY_APP_ITEM_NAME" + const val KEY_CONVERSATION_PASSWORD = "KEY_CONVERSATION_PASSWORD" + const val KEY_ROOM_TOKEN = "KEY_ROOM_TOKEN" + const val KEY_ROOM_ONE_TO_ONE = "KEY_ROOM_ONE_TO_ONE" + const val KEY_NEW_CONVERSATION = "KEY_NEW_CONVERSATION" + const val KEY_ADD_PARTICIPANTS = "KEY_ADD_PARTICIPANTS" + const val KEY_EXISTING_PARTICIPANTS = "KEY_EXISTING_PARTICIPANTS" + const val KEY_CALL_URL = "KEY_CALL_URL" + const val KEY_NEW_ROOM_NAME = "KEY_NEW_ROOM_NAME" + const val KEY_MODIFIED_BASE_URL = "KEY_MODIFIED_BASE_URL" + const val KEY_NOTIFICATION_SUBJECT = "KEY_NOTIFICATION_SUBJECT" + const val KEY_NOTIFICATION_SIGNATURE = "KEY_NOTIFICATION_SIGNATURE" + const val KEY_INTERNAL_USER_ID = "KEY_INTERNAL_USER_ID" + const val KEY_CONVERSATION_TYPE = "KEY_CONVERSATION_TYPE" + const val KEY_INVITED_PARTICIPANTS = "KEY_INVITED_PARTICIPANTS" + const val KEY_INVITED_CIRCLE = "KEY_INVITED_CIRCLE" + const val KEY_INVITED_GROUP = "KEY_INVITED_GROUP" + const val KEY_INVITED_EMAIL = "KEY_INVITED_EMAIL" + const val KEY_CONVERSATION_NAME = "KEY_CONVERSATION_NAME" + const val KEY_CONVERSATION_DISPLAY_NAME = "KEY_CONVERSATION_DISPLAY_NAME" + const val KEY_RECORDING_STATE = "KEY_RECORDING_STATE" + const val KEY_CALL_VOICE_ONLY = "KEY_CALL_VOICE_ONLY" + const val KEY_CALL_WITHOUT_NOTIFICATION = "KEY_CALL_WITHOUT_NOTIFICATION" + const val KEY_FROM_NOTIFICATION_START_CALL = "KEY_FROM_NOTIFICATION_START_CALL" + const val KEY_ROOM_ID = "KEY_ROOM_ID" + const val KEY_ARE_CALL_SOUNDS = "KEY_ARE_CALL_SOUNDS" + const val KEY_FILE_PATHS = "KEY_FILE_PATHS" + const val KEY_ACCOUNT = "KEY_ACCOUNT" + const val KEY_FILE_ID = "KEY_FILE_ID" + const val KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID" + const val KEY_NOTIFICATION_TIMESTAMP = "KEY_NOTIFICATION_TIMESTAMP" + const val KEY_SHARED_TEXT = "KEY_SHARED_TEXT" + const val KEY_GEOCODING_QUERY = "KEY_GEOCODING_QUERY" + const val KEY_META_DATA = "KEY_META_DATA" + const val KEY_FORWARD_MSG_FLAG = "KEY_FORWARD_MSG_FLAG" + const val KEY_FORWARD_MSG_TEXT = "KEY_FORWARD_MSG_TEXT" + const val KEY_FORWARD_HIDE_SOURCE_ROOM = "KEY_FORWARD_HIDE_SOURCE_ROOM" + const val KEY_SYSTEM_NOTIFICATION_ID = "KEY_SYSTEM_NOTIFICATION_ID" + const val KEY_MESSAGE_ID = "KEY_MESSAGE_ID" + const val KEY_MIME_TYPE_FILTER = "KEY_MIME_TYPE_FILTER" + const val KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO = "KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO" + const val KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO = "KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO" + const val KEY_IS_MODERATOR = "KEY_IS_MODERATOR" + const val KEY_SWITCH_TO_ROOM = "KEY_SWITCH_TO_ROOM" + const val KEY_START_CALL_AFTER_ROOM_SWITCH = "KEY_START_CALL_AFTER_ROOM_SWITCH" + const val KEY_IS_BREAKOUT_ROOM = "KEY_IS_BREAKOUT_ROOM" + const val KEY_NOTIFICATION_RESTRICT_DELETION = "KEY_NOTIFICATION_RESTRICT_DELETION" + const val KEY_DISMISS_RECORDING_URL = "KEY_DISMISS_RECORDING_URL" + const val KEY_SHARE_RECORDING_TO_CHAT_URL = "KEY_SHARE_RECORDING_TO_CHAT_URL" + const val KEY_GEOCODING_RESULT = "KEY_GEOCODING_RESULT" + const val ADD_ADDITIONAL_ACCOUNT = "ADD_ADDITIONAL_ACCOUNT" + const val SAVED_TRANSLATED_MESSAGE = "SAVED_TRANSLATED_MESSAGE" + const val KEY_REAUTHORIZE_ACCOUNT = "KEY_REAUTHORIZE_ACCOUNT" + const val KEY_PASSWORD = "KEY_PASSWORD" + const val KEY_REMOTE_TALK_SHARE = "KEY_REMOTE_TALK_SHARE" + const val KEY_CHAT_API_VERSION = "KEY_CHAT_API_VERSION" + const val KEY_CALL_FLAG = "KEY_CALL_FLAG" + const val KEY_CREDENTIALS: String = "KEY_CREDENTIALS" + const val KEY_FIELD_MAP: String = "KEY_FIELD_MAP" + const val KEY_CHAT_URL: String = "KEY_CHAT_URL" + const val KEY_SCROLL_TO_NOTIFICATION_CATEGORY: String = "KEY_SCROLL_TO_NOTIFICATION_CATEGORY" + const val KEY_FOCUS_INPUT: String = "KEY_FOCUS_INPUT" + const val KEY_THREAD_ID = "KEY_THREAD_ID" + const val KEY_FROM_QR: String = "KEY_FROM_QR" +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageModule.java b/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageModule.java new file mode 100644 index 0000000..e189218 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageModule.java @@ -0,0 +1,33 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.database.arbitrarystorage; + +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager; +import com.nextcloud.talk.dagger.modules.DatabaseModule; +import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository; + +import javax.inject.Inject; + +import autodagger.AutoInjector; +import dagger.Module; +import dagger.Provides; + +@Module(includes = DatabaseModule.class) +@AutoInjector(NextcloudTalkApplication.class) +public class ArbitraryStorageModule { + + @Inject + public ArbitraryStorageModule() { + } + + @Provides + public ArbitraryStorageManager provideArbitraryStorageManager(ArbitraryStoragesRepository repository) { + return new ArbitraryStorageManager(repository); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt new file mode 100644 index 0000000..e41c8ff --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderImpl.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.database.user + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.users.UserManager +import io.reactivex.Maybe +import io.reactivex.disposables.Disposable +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Listens to changes in the database and provides the current user without needing to query the database everytime. + */ +@Singleton +class CurrentUserProviderImpl @Inject constructor(private val userManager: UserManager) : CurrentUserProviderNew { + + private var _currentUser: User? = null + + // synchronized to avoid multiple observers initialized from different threads + @get:Synchronized + @set:Synchronized + private var currentUserObserver: Disposable? = null + + override val currentUser: Maybe + get() { + if (_currentUser == null) { + // immediately get a result synchronously + _currentUser = userManager.currentUser.blockingGet() + if (currentUserObserver == null) { + currentUserObserver = userManager.currentUserObservable + .subscribe { + _currentUser = it + } + } + } + return _currentUser?.let { Maybe.just(it) } ?: Maybe.empty() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderNew.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderNew.kt new file mode 100644 index 0000000..361ba58 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderNew.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.database.user + +import com.nextcloud.talk.data.user.model.User +import io.reactivex.Maybe + +interface CurrentUserProviderNew { + val currentUser: Maybe +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt new file mode 100644 index 0000000..1c846c6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.database.user + +import com.nextcloud.talk.dagger.modules.DatabaseModule +import com.nextcloud.talk.data.user.UsersRepository +import com.nextcloud.talk.users.UserManager +import dagger.Binds +import dagger.Module +import dagger.Provides + +@Module(includes = [DatabaseModule::class]) +abstract class UserModule { + + @Binds + abstract fun bindCurrentUserProviderNew(currentUserProviderImpl: CurrentUserProviderImpl): CurrentUserProviderNew + + companion object { + @Provides + fun provideUserManager(userRepository: UsersRepository): UserManager = UserManager(userRepository) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt new file mode 100644 index 0000000..6beeafe --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt @@ -0,0 +1,228 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Andy Scherzinger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.message + +import android.content.Context +import android.content.Intent +import android.graphics.Typeface +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.StyleSpan +import android.util.Log +import android.view.View +import androidx.core.net.toUri +import com.nextcloud.talk.R +import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.utils.DisplayUtils +import io.noties.markwon.AbstractMarkwonPlugin +import io.noties.markwon.Markwon +import io.noties.markwon.MarkwonConfiguration +import io.noties.markwon.core.MarkwonTheme +import io.noties.markwon.ext.strikethrough.StrikethroughPlugin +import io.noties.markwon.ext.tables.TablePlugin +import io.noties.markwon.ext.tasklist.TaskListDrawable +import io.noties.markwon.ext.tasklist.TaskListPlugin + +class MessageUtils(val context: Context) { + fun enrichChatReplyMessageText( + context: Context, + message: ChatMessage, + incoming: Boolean, + viewThemeUtils: ViewThemeUtils + ): Spanned? = + if (message.message == null) { + null + } else if (message.renderMarkdown == false) { + SpannableString(DisplayUtils.ellipsize(message.text, MAX_REPLY_LENGTH)) + } else { + enrichChatMessageText( + context, + DisplayUtils.ellipsize(message.text, MAX_REPLY_LENGTH), + incoming, + viewThemeUtils + ) + } + + fun enrichChatMessageText( + context: Context, + message: ChatMessage, + incoming: Boolean, + viewThemeUtils: ViewThemeUtils + ): Spanned? = + if (message.message == null) { + null + } else if (message.renderMarkdown == false) { + SpannableString(message.message) + } else { + val newMessage = message.message!!.replace("\n", " \n", false) + enrichChatMessageText(context, newMessage, incoming, viewThemeUtils) + } + + fun enrichChatMessageText( + context: Context, + message: String, + incoming: Boolean, + viewThemeUtils: ViewThemeUtils + ): Spanned = viewThemeUtils.talk.themeMarkdown(context, message, incoming) + + fun processMessageParameters( + themingContext: Context, + viewThemeUtils: ViewThemeUtils, + spannedText: Spanned, + message: ChatMessage, + itemView: View? + ): Spanned { + var processedMessageText = spannedText + val messageParameters = message.messageParameters + if (messageParameters != null && messageParameters.size > 0) { + processedMessageText = processMessageParameters( + themingContext, + viewThemeUtils, + messageParameters, + message, + processedMessageText, + itemView + ) + } + return processedMessageText + } + + @Suppress("NestedBlockDepth", "LongParameterList") + private fun processMessageParameters( + themingContext: Context, + viewThemeUtils: ViewThemeUtils, + messageParameters: HashMap>, + message: ChatMessage, + messageString: Spanned, + itemView: View? + ): Spanned { + var messageStringInternal = messageString + for (key in messageParameters.keys) { + val individualHashMap = message.messageParameters?.get(key) + if (individualHashMap != null) { + when (individualHashMap["type"]) { + "user", "guest", "call", "user-group", "email", "circle" -> { + val chip = if (individualHashMap["id"]?.equals(message.activeUser?.userId) == true) { + R.xml.chip_you + } else { + R.xml.chip_others + } + val id = if (individualHashMap["server"] != null) { + individualHashMap["id"] + "@" + individualHashMap["server"] + } else { + individualHashMap["id"] + } + + val name = individualHashMap["name"] + val type = individualHashMap["type"] + val user = message.activeUser + if (user == null || key == null) break + if (id == null || name == null || type == null) break + + messageStringInternal = DisplayUtils.searchAndReplaceWithMentionSpan( + key, + themingContext, + messageStringInternal, + id, + message.token, + name, + type, + user, + chip, + viewThemeUtils, + individualHashMap["server"] != null + ) + } + + "file" -> { + itemView?.setOnClickListener { v -> + val browserIntent = Intent(Intent.ACTION_VIEW, individualHashMap["link"]?.toUri()) + context.startActivity(browserIntent) + } + } + else -> { + messageStringInternal = defaultMessageParameters(messageStringInternal, individualHashMap, key) + } + } + } + } + return messageStringInternal + } + + fun processEditMessageParameters( + messageParameters: HashMap>, + message: ChatMessage?, + inputEditText: String + ): Spanned { + var result = inputEditText + for (key in messageParameters.keys) { + val individualHashMap = message?.messageParameters?.get(key) + if (individualHashMap != null) { + val mentionId = individualHashMap["mention-id"] + val type = individualHashMap["type"] + val name = individualHashMap["name"] + val placeholder = "@$name" + result = when (type) { + "user", "guest", "email" -> result.replace(placeholder, "@$mentionId", ignoreCase = false) + "user-group", "circle" -> result.replace(placeholder, "@\"$mentionId\"", ignoreCase = false) + "call" -> result.replace(placeholder, "@all", ignoreCase = false) + else -> result + } + } + } + return SpannableString(result) + } + + private fun defaultMessageParameters( + messageString: Spanned, + individualHashMap: HashMap, + key: String? + ): Spanned { + val spannable = SpannableStringBuilder(messageString) + val placeholder = "{$key}" + val replacementText = individualHashMap["name"] + var start = spannable.indexOf(placeholder) + while (start != -1) { + val end = start + placeholder.length + spannable.replace(start, end, replacementText) + spannable.setSpan( + StyleSpan(Typeface.BOLD), + start, + start + replacementText!!.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + start = spannable.indexOf(placeholder, start + replacementText.length) + } + return spannable + } + + fun getRenderedMarkdownText(context: Context, markdown: String, textColor: Int): Spanned { + val drawable = TaskListDrawable(textColor, textColor, context.getColor(R.color.bg_default)) + val markwon = Markwon.builder(context).usePlugin(object : AbstractMarkwonPlugin() { + override fun configureTheme(builder: MarkwonTheme.Builder) { + builder.isLinkUnderlined(true).headingBreakHeight(0) + } + + override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { + builder.linkResolver { view: View?, link: String? -> + Log.i(TAG, "Link action not implemented $view / $link") + } + } + }) + .usePlugin(TaskListPlugin.create(drawable)) + .usePlugin(TablePlugin.create { _ -> }) + .usePlugin(StrikethroughPlugin.create()).build() + return markwon.toMarkdown(markdown) + } + + companion object { + private const val TAG = "MessageUtils" + const val MAX_REPLY_LENGTH = 250 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/message/SendMessageUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/message/SendMessageUtils.kt new file mode 100644 index 0000000..547cc6c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/message/SendMessageUtils.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.message + +import java.security.MessageDigest +import java.util.Calendar +import java.util.UUID + +class SendMessageUtils { + fun generateReferenceId(): String { + val randomString = UUID.randomUUID().toString() + val digest = MessageDigest.getInstance("SHA-256") + val hashBytes = digest.digest(randomString.toByteArray(Charsets.UTF_8)) + return hashBytes.joinToString("") { "%02x".format(it) } + } + + @Suppress("MagicNumber") + fun removeYearFromTimestamp(timestampMillis: Long): Int { + val calendar = Calendar.getInstance().apply { timeInMillis = timestampMillis } + + val month = calendar.get(Calendar.MONTH) + val day = calendar.get(Calendar.DAY_OF_MONTH) + val hour = calendar.get(Calendar.HOUR_OF_DAY) + val minute = calendar.get(Calendar.MINUTE) + val second = calendar.get(Calendar.SECOND) + return (month * 1000000) + (day * 10000) + (hour * 100) + (minute * 10) + second + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtil.kt b/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtil.kt new file mode 100644 index 0000000..e285902 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtil.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.permissions + +interface PlatformPermissionUtil { + val privateBroadcastPermission: String + fun isCameraPermissionGranted(): Boolean + fun isMicrophonePermissionGranted(): Boolean + fun isBluetoothPermissionGranted(): Boolean + fun isFilesPermissionGranted(): Boolean + fun isPostNotificationsPermissionGranted(): Boolean + fun isLocationPermissionGranted(): Boolean +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt new file mode 100644 index 0000000..a939561 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt @@ -0,0 +1,103 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.permissions + +import android.Manifest +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.PermissionChecker +import com.nextcloud.talk.BuildConfig + +class PlatformPermissionUtilImpl(private val context: Context) : PlatformPermissionUtil { + override val privateBroadcastPermission: String = + "${BuildConfig.APPLICATION_ID}.${BuildConfig.PERMISSION_LOCAL_BROADCAST}" + + override fun isCameraPermissionGranted(): Boolean = + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) == PermissionChecker.PERMISSION_GRANTED + + @RequiresApi(Build.VERSION_CODES.S) + override fun isBluetoothPermissionGranted(): Boolean = + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT + ) == PermissionChecker.PERMISSION_GRANTED + + override fun isMicrophonePermissionGranted(): Boolean = + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.RECORD_AUDIO + ) == PermissionChecker.PERMISSION_GRANTED + + override fun isFilesPermissionGranted(): Boolean = + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { + if ( + PermissionChecker.checkSelfPermission(context, Manifest.permission.READ_MEDIA_IMAGES) + == PermissionChecker.PERMISSION_GRANTED || + PermissionChecker.checkSelfPermission(context, Manifest.permission.READ_MEDIA_VIDEO) + == PermissionChecker.PERMISSION_GRANTED || + PermissionChecker.checkSelfPermission(context, Manifest.permission.READ_MEDIA_AUDIO) + == PermissionChecker.PERMISSION_GRANTED + ) { + Log.d(TAG, "Permission is granted (SDK 33 or greater)") + true + } else { + Log.d(TAG, "Permission is revoked (SDK 33 or greater)") + false + } + } + Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> { + if (PermissionChecker.checkSelfPermission( + context, + Manifest.permission.READ_EXTERNAL_STORAGE + ) == PermissionChecker.PERMISSION_GRANTED + ) { + Log.d(TAG, "Permission is granted (SDK 30 or greater)") + true + } else { + Log.d(TAG, "Permission is revoked (SDK 30 or greater)") + false + } + } + else -> { + if (PermissionChecker.checkSelfPermission( + context, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PermissionChecker.PERMISSION_GRANTED + ) { + Log.d(TAG, "Permission is granted") + true + } else { + Log.d(TAG, "Permission is revoked") + false + } + } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun isPostNotificationsPermissionGranted(): Boolean = + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PermissionChecker.PERMISSION_GRANTED + + override fun isLocationPermissionGranted(): Boolean = + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PermissionChecker.PERMISSION_GRANTED + + companion object { + private val TAG = PlatformPermissionUtilImpl::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt new file mode 100644 index 0000000..d03033f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/power/PowerManagerUtils.kt @@ -0,0 +1,176 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-FileCopyrightText: 2017-2018 Stuart O. Anderson + * SPDX-License-Identifier: GPL-3.0-or-later + * + * This class is in part based on the code from the great people that wrote Signal + * https://github.com/signalapp/Signal-Android/raw/f9adb4e4554a44fd65b77320e34bf4bccf7924ce/src/org/thoughtcrime/securesms/webrtc/locks/LockManager.java + */ +package com.nextcloud.talk.utils.power + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.POWER_SERVICE +import android.content.res.Configuration +import android.net.wifi.WifiManager +import android.net.wifi.WifiManager.WifiLock +import android.os.PowerManager +import android.provider.Settings +import autodagger.AutoInjector +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PowerManagerUtils { + private val fullLock: PowerManager.WakeLock + private val partialLock: PowerManager.WakeLock + private val wifiLock: WifiLock + private val wifiLockEnforced: Boolean + + @JvmField + @Inject + var context: Context? = null + private val proximityLock: ProximityLock + private var proximityDisabled = false + private var orientation: Int + + init { + sharedApplication!!.componentApplication.inject(this) + val pm = context!!.getSystemService(POWER_SERVICE) as PowerManager + fullLock = pm.newWakeLock( + PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, + "nctalk:fullwakelock" + ) + partialLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "nctalk:partialwakelock") + proximityLock = ProximityLock(pm) + + // we suppress a possible leak because this is indeed application context + @SuppressLint("WifiManagerPotentialLeak") + val wm = + context!!.getSystemService(Context.WIFI_SERVICE) as WifiManager + wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "nctalk:wifiwakelock") + fullLock.setReferenceCounted(false) + partialLock.setReferenceCounted(false) + wifiLock.setReferenceCounted(false) + wifiLockEnforced = isWifiPowerActiveModeEnabled(context) + orientation = context!!.resources.configuration.orientation + } + + fun isIgnoringBatteryOptimizations(): Boolean { + val packageName = context!!.packageName + val pm = context!!.getSystemService(POWER_SERVICE) as PowerManager + return pm.isIgnoringBatteryOptimizations(packageName) + } + + fun setOrientation(newOrientation: Int) { + orientation = newOrientation + updateInCallWakeLockState() + } + + fun updatePhoneState(state: PhoneState?) { + when (state) { + PhoneState.IDLE -> setWakeLockState(WakeLockState.SLEEP) + PhoneState.PROCESSING -> setWakeLockState(WakeLockState.PARTIAL) + PhoneState.INTERACTIVE -> setWakeLockState(WakeLockState.FULL) + PhoneState.WITH_PROXIMITY_SENSOR_LOCK -> { + proximityDisabled = false + updateInCallWakeLockState() + } + + PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK -> { + proximityDisabled = true + updateInCallWakeLockState() + } + + else -> {} + } + } + + private fun updateInCallWakeLockState() { + if (orientation != Configuration.ORIENTATION_LANDSCAPE && wifiLockEnforced && !proximityDisabled) { + setWakeLockState(WakeLockState.PROXIMITY) + } else { + setWakeLockState(WakeLockState.FULL) + } + } + + private fun isWifiPowerActiveModeEnabled(context: Context?): Boolean { + val wifiPowerActiveMode = Settings.Secure.getInt(context!!.contentResolver, "wifi_pwr_active_mode", -1) + return wifiPowerActiveMode != 0 + } + + @SuppressLint("WakelockTimeout") + @Synchronized + private fun setWakeLockState(newState: WakeLockState) { + when (newState) { + WakeLockState.FULL -> { + if (!fullLock.isHeld) { + fullLock.acquire() + } + if (!partialLock.isHeld) { + partialLock.acquire() + } + if (!wifiLock.isHeld) { + wifiLock.acquire() + } + proximityLock.release() + } + + WakeLockState.PARTIAL -> { + if (!partialLock.isHeld) { + partialLock.acquire() + } + if (!wifiLock.isHeld) { + wifiLock.acquire() + } + fullLock.release() + proximityLock.release() + } + + WakeLockState.SLEEP -> { + fullLock.release() + partialLock.release() + wifiLock.release() + proximityLock.release() + } + + WakeLockState.PROXIMITY -> { + if (!partialLock.isHeld) { + partialLock.acquire() + } + if (!wifiLock.isHeld) { + wifiLock.acquire() + } + fullLock.release() + proximityLock.acquire() + } + + else -> {} + } + } + + enum class PhoneState { + IDLE, + PROCESSING, + + // used when the phone is active but before the user should be alerted. + INTERACTIVE, + WITHOUT_PROXIMITY_SENSOR_LOCK, + WITH_PROXIMITY_SENSOR_LOCK + } + + enum class WakeLockState { + FULL, + PARTIAL, + SLEEP, + PROXIMITY + } + + companion object { + private val TAG = PowerManagerUtils::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/power/ProximityLock.java b/app/src/main/java/com/nextcloud/talk/utils/power/ProximityLock.java new file mode 100644 index 0000000..b3471b0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/power/ProximityLock.java @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.power; + +import android.annotation.SuppressLint; +import android.os.PowerManager; + +import java.util.Optional; + +class ProximityLock { + private final Optional proximityLock; + + ProximityLock(PowerManager pm) { + proximityLock = getProximityLock(pm); + } + + private Optional getProximityLock(PowerManager powerManager) { + if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { + return Optional.ofNullable(powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "nctalk:proximitylock")); + } else { + return Optional.empty(); + } + } + + @SuppressLint("WakelockTimeout") + void acquire() { + if (!proximityLock.isPresent() || proximityLock.get().isHeld()) { + return; + } + + proximityLock.get().acquire(); + } + + void release() { + if (!proximityLock.isPresent() || !proximityLock.get().isHeld()) { + return; + } + + proximityLock.get().release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java new file mode 100644 index 0000000..e68e129 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java @@ -0,0 +1,191 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.preferences; + +import android.annotation.SuppressLint; + +import com.nextcloud.talk.ui.PlaybackSpeed; + +import kotlin.Pair; + +@SuppressLint("NonConstantResourceId") +public interface AppPreferences { + + String getProxyType(); + + void setProxyType(String proxyType); + + void removeProxyType(); + + String getProxyHost(); + + void setProxyHost(String proxyHost); + + void removeProxyHost(); + + String getProxyPort(); + + void setProxyPort(String proxyPort); + + void removeProxyPort(); + + boolean getProxyCredentials(); + + void setProxyNeedsCredentials(boolean proxyNeedsCredentials); + + void removeProxyCredentials(); + + String getProxyUsername(); + + void setProxyUsername(String proxyUsername); + + void removeProxyUsername(); + + String getProxyPassword(); + + void setProxyPassword(String proxyPassword); + + void removeProxyPassword(); + + String getPushToken(); + + void setPushToken(String pushToken); + + Long getPushTokenLatestGeneration(); + + void setPushTokenLatestGeneration(Long date); + + Long getPushTokenLatestFetch(); + + void setPushTokenLatestFetch(Long date); + + void removePushToken(); + + String getTemporaryClientCertAlias(); + + void setTemporaryClientCertAlias(String alias); + + void removeTemporaryClientCertAlias(); + + boolean getPushToTalkIntroShown(); + + void setPushToTalkIntroShown(boolean shown); + + void removePushToTalkIntroShown(); + + String getCallRingtoneUri(); + + void setCallRingtoneUri(String value); + + void removeCallRingtoneUri(); + + String getMessageRingtoneUri(); + + void setMessageRingtoneUri(String value); + + void removeMessageRingtoneUri(); + + boolean getIsNotificationChannelUpgradedToV2(); + + void setNotificationChannelIsUpgradedToV2(boolean value); + + void removeNotificationChannelUpgradeToV2(); + + boolean getIsNotificationChannelUpgradedToV3(); + + void setNotificationChannelIsUpgradedToV3(boolean value); + + void removeNotificationChannelUpgradeToV3(); + + boolean getIsScreenSecured(); + + void setScreenSecurity(boolean value); + + void removeScreenSecurity(); + + boolean getIsScreenLocked(); + + void setScreenLock(boolean value); + + void removeScreenLock(); + + boolean getIsKeyboardIncognito(); + + void setIncognitoKeyboard(boolean value); + + void removeIncognitoKeyboard(); + + boolean isPhoneBookIntegrationEnabled(); + + void setPhoneBookIntegration(boolean value); + + // TODO Remove in 13.0.0 + void removeLinkPreviews(); + + String getScreenLockTimeout(); + + void setScreenLockTimeout(String value); + + void removeScreenLockTimeout(); + + String getTheme(); + + void setTheme(String newValue); + + void removeTheme(); + + boolean isDbCypherToUpgrade(); + + void setDbCypherToUpgrade(boolean value); + + void setPhoneBookIntegrationLastRun(long currentTimeMillis); + + long getPhoneBookIntegrationLastRun(Long defaultValue); + + void setReadPrivacy(boolean value); + + boolean getReadPrivacy(); + + void setTypingStatus(boolean value); + + boolean getTypingStatus(); + + void setSorting(String value); + + String getSorting(); + + void saveWaveFormForFile(String filename, Float[] array); + + Float[] getWaveFormFromFile(String filename); + + void saveLastKnownId(String internalConversationId, int lastReadId); + + int getLastKnownId(String internalConversationId, int defaultValue); + + void deleteAllMessageQueuesFor(String userId); + + void savePreferredPlayback(String userId, PlaybackSpeed speed); + + PlaybackSpeed getPreferredPlayback(String userId); + + Long getNotificationWarningLastPostponedDate(); + + void setNotificationWarningLastPostponedDate(Long showNotificationWarning); + + Boolean getShowRegularNotificationWarning(); + + void setShowRegularNotificationWarning(boolean value); + + void setConversationListPositionAndOffset(int position, int offset); + + Pair getConversationListPositionAndOffset(); + + void clear(); +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt new file mode 100644 index 0000000..61d4b79 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferencesImpl.kt @@ -0,0 +1,649 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Christian Reiner + * SPDX-FileCopyrightText: 2023 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.preferences + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.nextcloud.talk.R +import com.nextcloud.talk.ui.PlaybackSpeed +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking + +@ExperimentalCoroutinesApi +@Suppress("TooManyFunctions", "DeferredResultUnused", "EmptyFunctionBlock") +class AppPreferencesImpl(val context: Context) : AppPreferences { + + override fun getProxyType(): String = + runBlocking { + async { readString(PROXY_TYPE, context.resources.getString(R.string.nc_no_proxy)).first() } + }.getCompleted() + + override fun setProxyType(proxyType: String?) = + runBlocking { + async { + if (proxyType != null) { + writeString(PROXY_TYPE, proxyType) + } + } + } + + override fun removeProxyType() { + proxyType = "" + } + + override fun getProxyHost(): String = runBlocking { async { readString(PROXY_HOST).first() } }.getCompleted() + + override fun setProxyHost(proxyHost: String?) = + runBlocking { + async { + if (proxyHost != null) { + writeString(PROXY_HOST, proxyHost) + } + } + } + + override fun removeProxyHost() { + proxyHost = "" + } + + override fun getProxyPort(): String = runBlocking { async { readString(PROXY_PORT).first() } }.getCompleted() + + override fun setProxyPort(proxyPort: String?) = + runBlocking { + async { + if (proxyPort != null) { + writeString(PROXY_PORT, proxyPort) + } + } + } + + override fun removeProxyPort() { + proxyPort = "" + } + + override fun getProxyCredentials(): Boolean = + runBlocking { + async { readBoolean(PROXY_CRED).first() } + }.getCompleted() + + override fun setProxyNeedsCredentials(proxyNeedsCredentials: Boolean) = + runBlocking { + async { + writeBoolean(PROXY_CRED, proxyNeedsCredentials) + } + } + + override fun removeProxyCredentials() { + setProxyNeedsCredentials(false) + } + + override fun getProxyUsername(): String = + runBlocking { + async { readString(PROXY_USERNAME).first() } + }.getCompleted() + + override fun setProxyUsername(proxyUsername: String?) = + runBlocking { + async { + if (proxyUsername != null) { + writeString(PROXY_USERNAME, proxyUsername) + } + } + } + + override fun removeProxyUsername() { + proxyUsername = "" + } + + override fun getProxyPassword(): String = + runBlocking { + async { readString(PROXY_PASSWORD).first() } + }.getCompleted() + + override fun setProxyPassword(proxyPassword: String?) = + runBlocking { + async { + if (proxyPassword != null) { + writeString(PROXY_PASSWORD, proxyPassword) + } + } + } + + override fun removeProxyPassword() { + proxyPassword = "" + } + + override fun getPushToken(): String = runBlocking { async { readString(PUSH_TOKEN).first() } }.getCompleted() + + override fun setPushToken(pushToken: String?) = + runBlocking { + async { + if (pushToken != null) { + writeString(PUSH_TOKEN, pushToken) + } + } + } + + override fun removePushToken() { + pushToken = "" + } + + override fun getPushTokenLatestGeneration(): Long = + runBlocking { + async { readLong(PUSH_TOKEN_LATEST_GENERATION).first() } + }.getCompleted() + + override fun setPushTokenLatestGeneration(date: Long) = + runBlocking { + async { + writeLong(PUSH_TOKEN_LATEST_GENERATION, date) + } + } + + override fun getPushTokenLatestFetch(): Long = + runBlocking { + async { readLong(PUSH_TOKEN_LATEST_FETCH).first() } + }.getCompleted() + + override fun setPushTokenLatestFetch(date: Long) = + runBlocking { + async { + writeLong(PUSH_TOKEN_LATEST_FETCH, date) + } + } + + override fun getTemporaryClientCertAlias(): String = + runBlocking { + async { readString(TEMP_CLIENT_CERT_ALIAS).first() } + }.getCompleted() + + override fun setTemporaryClientCertAlias(alias: String?) = + runBlocking { + async { + if (alias != null) { + writeString(TEMP_CLIENT_CERT_ALIAS, alias) + } + } + } + + override fun removeTemporaryClientCertAlias() { + temporaryClientCertAlias = "" + } + + override fun getPushToTalkIntroShown(): Boolean = + runBlocking { + async { readBoolean(PUSH_TO_TALK_INTRO_SHOWN).first() } + }.getCompleted() + + override fun setPushToTalkIntroShown(shown: Boolean) = + runBlocking { + async { + writeBoolean(PUSH_TO_TALK_INTRO_SHOWN, shown) + } + } + + override fun removePushToTalkIntroShown() { + pushToTalkIntroShown = false + } + + override fun getCallRingtoneUri(): String = + runBlocking { + async { readString(CALL_RINGTONE).first() } + }.getCompleted() + + override fun setCallRingtoneUri(value: String?) = + runBlocking { + async { + if (value != null) { + writeString(CALL_RINGTONE, value) + } + } + } + + override fun removeCallRingtoneUri() { + callRingtoneUri = "" + } + + override fun getMessageRingtoneUri(): String = + runBlocking { + async { readString(MESSAGE_RINGTONE).first() } + }.getCompleted() + + override fun setMessageRingtoneUri(value: String?) = + runBlocking { + async { + if (value != null) { + writeString(MESSAGE_RINGTONE, value) + } + } + } + + override fun removeMessageRingtoneUri() { + messageRingtoneUri = "" + } + + override fun getIsNotificationChannelUpgradedToV2(): Boolean = + runBlocking { + async { readBoolean(NOTIFY_UPGRADE_V2).first() } + }.getCompleted() + + override fun setNotificationChannelIsUpgradedToV2(value: Boolean) = + runBlocking { + async { + writeBoolean(NOTIFY_UPGRADE_V2, value) + } + } + + override fun removeNotificationChannelUpgradeToV2() { + setNotificationChannelIsUpgradedToV2(false) + } + + override fun getIsNotificationChannelUpgradedToV3(): Boolean = + runBlocking { + async { readBoolean(NOTIFY_UPGRADE_V3).first() } + }.getCompleted() + + override fun setNotificationChannelIsUpgradedToV3(value: Boolean) = + runBlocking { + async { + writeBoolean(NOTIFY_UPGRADE_V3, value) + } + } + + override fun removeNotificationChannelUpgradeToV3() { + setNotificationChannelIsUpgradedToV3(false) + } + + override fun getIsScreenSecured(): Boolean = + runBlocking { + async { readBoolean(SCREEN_SECURITY).first() } + }.getCompleted() + + override fun setScreenSecurity(value: Boolean) = + runBlocking { + async { + writeBoolean(SCREEN_SECURITY, value) + } + } + + override fun removeScreenSecurity() { + setScreenSecurity(false) + } + + override fun getIsScreenLocked(): Boolean = + runBlocking { + async { readBoolean(SCREEN_LOCK).first() } + }.getCompleted() + + override fun setScreenLock(value: Boolean) = + runBlocking { + async { + writeBoolean(SCREEN_LOCK, value) + } + } + + override fun removeScreenLock() { + setScreenLock(false) + } + + override fun getIsKeyboardIncognito(): Boolean { + val read = runBlocking { async { readBoolean(INCOGNITO_KEYBOARD).first() } }.getCompleted() + return read + } + + override fun setIncognitoKeyboard(value: Boolean) = + runBlocking { + async { + writeBoolean(INCOGNITO_KEYBOARD, value) + } + } + + override fun removeIncognitoKeyboard() { + setIncognitoKeyboard(false) + } + + override fun isPhoneBookIntegrationEnabled(): Boolean = + runBlocking { + async { readBoolean(PHONE_BOOK_INTEGRATION).first() } + }.getCompleted() + + override fun setPhoneBookIntegration(value: Boolean) = + runBlocking { + async { + writeBoolean(PHONE_BOOK_INTEGRATION, value) + } + } + + override fun removeLinkPreviews() = + runBlocking { + async { + writeBoolean(LINK_PREVIEWS, false) + } + } + + override fun getScreenLockTimeout(): String { + val default = context.resources.getString(R.string.nc_screen_lock_timeout_sixty) + val read = runBlocking { async { readString(SCREEN_LOCK_TIMEOUT).first() } }.getCompleted() + return read.ifEmpty { default } + } + + override fun setScreenLockTimeout(value: String?) = + runBlocking { + async { + if (value != null) { + writeString(SCREEN_LOCK_TIMEOUT, value) + } + } + } + + override fun removeScreenLockTimeout() { + screenLockTimeout = "" + } + + override fun getTheme(): String { + val key = context.resources.getString(R.string.nc_settings_theme_key) + val default = context.resources.getString(R.string.nc_default_theme) + val read = runBlocking { async { readString(key).first() } }.getCompleted() + return read.ifEmpty { default } + } + + override fun setTheme(value: String?) = + runBlocking { + async { + if (value != null) { + val key = context.resources.getString(R.string.nc_settings_theme_key) + writeString(key, value) + } + } + } + + override fun removeTheme() { + theme = "" + } + + override fun isDbCypherToUpgrade(): Boolean { + val read = runBlocking { async { readBoolean(DB_CYPHER_V4_UPGRADE).first() } }.getCompleted() + return read + } + + override fun setDbCypherToUpgrade(value: Boolean) = + runBlocking { + async { + writeBoolean(DB_CYPHER_V4_UPGRADE, value) + } + } + + override fun getShowRegularNotificationWarning(): Boolean = + runBlocking { + async { readBoolean(SHOW_REGULAR_NOTIFICATION_WARNING, true).first() } + }.getCompleted() + + override fun setShowRegularNotificationWarning(value: Boolean) = + runBlocking { + async { + writeBoolean(SHOW_REGULAR_NOTIFICATION_WARNING, value) + } + } + + override fun setConversationListPositionAndOffset(position: Int, offset: Int) { + runBlocking { + async { + writeString(CONVERSATION_LIST_POSITION_OFFSET, "$position,$offset") + } + } + } + + override fun getConversationListPositionAndOffset(): Pair { + val pairString = runBlocking { + async { readString(CONVERSATION_LIST_POSITION_OFFSET).first() } + }.getCompleted() + + if (pairString.isEmpty()) return Pair(0, 0) + + val pairArr = pairString.split(',') + val position = pairArr[0].toInt() + val offset = pairArr[1].toInt() + + return Pair(position, offset) + } + + override fun setPhoneBookIntegrationLastRun(currentTimeMillis: Long) = + runBlocking { + async { + writeLong(PHONE_BOOK_INTEGRATION_LAST_RUN, currentTimeMillis) + } + } + + override fun getPhoneBookIntegrationLastRun(defaultValue: Long?): Long { + val result = if (defaultValue != null) { + runBlocking { async { readLong(PHONE_BOOK_INTEGRATION_LAST_RUN, defaultValue = defaultValue).first() } } + .getCompleted() + } else { + runBlocking { async { readLong(PHONE_BOOK_INTEGRATION_LAST_RUN).first() } }.getCompleted() + } + return result + } + + override fun setReadPrivacy(value: Boolean) = + runBlocking { + val key = context.resources.getString(R.string.nc_settings_read_privacy_key) + async { + writeBoolean(key, value) + } + } + + override fun getReadPrivacy(): Boolean { + val key = context.resources.getString(R.string.nc_settings_read_privacy_key) + return runBlocking { async { readBoolean(key).first() } }.getCompleted() + } + + override fun setTypingStatus(value: Boolean) = + runBlocking { + async { + writeBoolean(TYPING_STATUS, value) + } + } + + override fun getTypingStatus(): Boolean = + runBlocking { + async { readBoolean(TYPING_STATUS).first() } + }.getCompleted() + + override fun setSorting(value: String?) = + runBlocking { + val key = context.resources.getString(R.string.nc_file_browser_sort_by_key) + async { + if (value != null) { + writeString(key, value) + } + } + } + + override fun getSorting(): String { + val key = context.resources.getString(R.string.nc_file_browser_sort_by_key) + val default = context.resources.getString(R.string.nc_file_browser_sort_by_default) + val read = runBlocking { async { readString(key).first() } }.getCompleted() + return read.ifEmpty { default } + } + + override fun saveWaveFormForFile(filename: String, array: Array) = + runBlocking { + async { + writeString(filename, array.contentToString()) + } + } + + override fun getWaveFormFromFile(filename: String): Array { + val string = runBlocking { async { readString(filename).first() } }.getCompleted() + return if (string.isNotEmpty()) string.convertStringToArray() else floatArrayOf().toTypedArray() + } + + override fun saveLastKnownId(internalConversationId: String, lastReadId: Int) { + runBlocking { + async { + writeString(internalConversationId, lastReadId.toString()) + } + } + } + + override fun getLastKnownId(internalConversationId: String, defaultValue: Int): Int { + val lastReadId = runBlocking { async { readString(internalConversationId).first() } }.getCompleted() + return if (lastReadId.isNotEmpty()) lastReadId.toInt() else defaultValue + } + + override fun deleteAllMessageQueuesFor(userId: String) { + runBlocking { + async { + val keyList = mutableListOf>() + val preferencesMap = context.dataStore.data.first().asMap() + for (preference in preferencesMap) { + if (preference.key.name.contains("$userId@")) { + keyList.add(preference.key) + } + } + + for (key in keyList) { + context.dataStore.edit { + it.remove(key) + } + } + } + } + } + + override fun savePreferredPlayback(userId: String, speed: PlaybackSpeed) { + runBlocking { + async { + writeString(userId + PLAY_BACK, speed.name) + } + } + } + + override fun getPreferredPlayback(userId: String): PlaybackSpeed = + runBlocking { + async { + val name = readString(userId + PLAY_BACK).first() + return@async if (name == "") PlaybackSpeed.NORMAL else PlaybackSpeed.byName(name) + } + }.getCompleted() + + override fun getNotificationWarningLastPostponedDate(): Long = + runBlocking { + async { readLong(LAST_NOTIFICATION_WARNING).first() } + }.getCompleted() + + override fun setNotificationWarningLastPostponedDate(showNotificationWarning: Long) = + runBlocking { + async { + writeLong(LAST_NOTIFICATION_WARNING, showNotificationWarning) + } + } + + override fun clear() {} + + private suspend fun writeString(key: String, value: String) = + context.dataStore.edit { settings -> + settings[ + stringPreferencesKey( + key + ) + ] = value + } + + /** + * Returns a Flow of type String + * @param key the key of the persisted data to be observed + */ + fun readString(key: String, defaultValue: String = ""): Flow = + context.dataStore.data.map { preferences -> + preferences[stringPreferencesKey(key)] ?: defaultValue + } + + private suspend fun writeBoolean(key: String, value: Boolean) = + context.dataStore.edit { settings -> + settings[ + booleanPreferencesKey( + key + ) + ] = value + } + + /** + * Returns a Flow of type Boolean + * @param key the key of the persisted data to be observed + */ + fun readBoolean(key: String, defaultValue: Boolean = false): Flow = + context.dataStore.data.map { preferences -> + preferences[booleanPreferencesKey(key)] ?: defaultValue + } + + private suspend fun writeLong(key: String, value: Long) = + context.dataStore.edit { settings -> + settings[longPreferencesKey(key)] = value + } + + private fun readLong(key: String, defaultValue: Long = 0): Flow = + context.dataStore.data.map { preferences -> + preferences[longPreferencesKey(key)] ?: defaultValue + } + + companion object { + @Suppress("UnusedPrivateProperty") + private val TAG = AppPreferencesImpl::class.simpleName + private val Context.dataStore: DataStore by preferencesDataStore(name = "settings") + const val PROXY_TYPE = "proxy_type" + const val PROXY_HOST = "proxy_host" + const val PROXY_PORT = "proxy_port" + const val PROXY_CRED = "proxy_credentials" + const val PROXY_USERNAME = "proxy_username" + const val PROXY_PASSWORD = "proxy_password" + const val PUSH_TOKEN = "push_token" + const val PUSH_TOKEN_LATEST_GENERATION = "push_token_latest_generation" + const val PUSH_TOKEN_LATEST_FETCH = "push_token_latest_fetch" + const val TEMP_CLIENT_CERT_ALIAS = "tempClientCertAlias" + const val PUSH_TO_TALK_INTRO_SHOWN = "pushToTalk_intro_shown" + const val CALL_RINGTONE = "call_ringtone" + const val MESSAGE_RINGTONE = "message_ringtone" + const val NOTIFY_UPGRADE_V2 = "notification_channels_upgrade_to_v2" + const val NOTIFY_UPGRADE_V3 = "notification_channels_upgrade_to_v3" + const val SCREEN_SECURITY = "screen_security" + const val SCREEN_LOCK = "screen_lock" + const val INCOGNITO_KEYBOARD = "incognito_keyboard" + const val PHONE_BOOK_INTEGRATION = "phone_book_integration" + const val LINK_PREVIEWS = "link_previews" + const val SCREEN_LOCK_TIMEOUT = "screen_lock_timeout" + const val DB_CYPHER_V4_UPGRADE = "db_cypher_v4_upgrade" + const val DB_ROOM_MIGRATED = "db_room_migrated" + const val PHONE_BOOK_INTEGRATION_LAST_RUN = "phone_book_integration_last_run" + const val TYPING_STATUS = "typing_status" + const val MESSAGE_QUEUE = "@message_queue" + const val PLAY_BACK = "_playback" + const val VOICE_MESSAGE_PLAYBACK_SPEEDS = "voice_message_playback_speeds" + const val SHOW_REGULAR_NOTIFICATION_WARNING = "show_regular_notification_warning" + const val LAST_NOTIFICATION_WARNING = "last_notification_warning" + const val CONVERSATION_LIST_POSITION_OFFSET = "CONVERSATION_LIST_POSITION_OFFSET" + private fun String.convertStringToArray(): Array { + var varString = this + val floatList = mutableListOf() + varString = varString.replace("\\[".toRegex(), "") + varString = varString.replace("]".toRegex(), "") + varString.split(",").forEach { floatList.add(it.toFloat()) } + return floatList.toTypedArray() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt new file mode 100644 index 0000000..449858e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt @@ -0,0 +1,200 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2021 Tim Krüger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.preferences.preferencestorage + +import android.text.TextUtils +import android.util.Log +import autodagger.AutoInjector +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager +import com.nextcloud.talk.data.storage.model.ArbitraryStorage +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ApiUtils.getConversationApiVersion +import com.nextcloud.talk.utils.ApiUtils.getCredentials +import com.nextcloud.talk.utils.ApiUtils.getUrlForMessageExpiration +import com.nextcloud.talk.utils.ApiUtils.getUrlForRoomNotificationCalls +import com.nextcloud.talk.utils.ApiUtils.getUrlForRoomNotificationLevel +import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.SpreedFeatures +import com.nextcloud.talk.utils.UserIdUtils.getIdForUser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class DatabaseStorageModule(conversationUser: User, conversationToken: String) { + + @JvmField + @Inject + var arbitraryStorageManager: ArbitraryStorageManager? = null + + @JvmField + @Inject + var ncApi: NcApi? = null + + @JvmField + @Inject + var ncApiCoroutines: NcApiCoroutines? = null + + private var messageExpiration = 0 + private val conversationUser: User + private val conversationToken: String + private val accountIdentifier: Long + + private var lobbyValue = false + + private var messageNotificationLevel: String? = null + + init { + sharedApplication!!.componentApplication.inject(this) + + this.conversationUser = conversationUser + this.accountIdentifier = getIdForUser(conversationUser) + this.conversationToken = conversationToken + } + + @Suppress("Detekt.TooGenericExceptionCaught") + suspend fun saveBoolean(key: String, value: Boolean) { + if ("call_notifications_switch" == key) { + val apiVersion = getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4)) + val url = getUrlForRoomNotificationCalls(apiVersion, conversationUser.baseUrl, conversationToken) + val credentials = getCredentials(conversationUser.username, conversationUser.token) + val notificationLevel = if (value) 1 else 0 + withContext(Dispatchers.IO) { + try { + ncApiCoroutines!!.notificationCalls(credentials!!, url, notificationLevel) + Log.d(TAG, "Toggled notification calls") + } catch (e: Exception) { + Log.e(TAG, "Error when trying to toggle notification calls", e) + } + } + } + if ("lobby_switch" != key) { + arbitraryStorageManager!!.storeStorageSetting( + accountIdentifier, + key, + value.toString(), + conversationToken + ) + } else { + lobbyValue = value + } + } + + @Suppress("Detekt.TooGenericExceptionCaught") + suspend fun saveString(key: String, value: String) { + when (key) { + "conversation_settings_dropdown" -> { + try { + val apiVersion = getConversationApiVersion(conversationUser, intArrayOf(API_VERSION_4)) + val trimmedValue = value.replace("expire_", "") + val valueInt = trimmedValue.toInt() + withContext(Dispatchers.IO) { + ncApiCoroutines!!.setMessageExpiration( + getCredentials(conversationUser.username, conversationUser.token)!!, + getUrlForMessageExpiration(apiVersion, conversationUser.baseUrl, conversationToken), + valueInt + ) + messageExpiration = valueInt + } + } catch (exception: Exception) { + Log.e(TAG, "Error when trying to set message expiration", exception) + } + } + "conversation_info_message_notifications_dropdown" -> { + try { + if (hasSpreedFeatureCapability( + conversationUser.capabilities!!.spreedCapability!!, + SpreedFeatures.NOTIFICATION_LEVELS + ) + ) { + if (TextUtils.isEmpty(messageNotificationLevel) || messageNotificationLevel != value) { + val intValue = when (value) { + "never" -> NOTIFICATION_NEVER + "mention" -> NOTIFICATION_MENTION + "always" -> NOTIFICATION_ALWAYS + else -> 0 + } + val apiVersion = getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.API_V4, 1)) + withContext(Dispatchers.IO) { + ncApiCoroutines!!.setNotificationLevel( + getCredentials(conversationUser.username, conversationUser.token)!!, + getUrlForRoomNotificationLevel( + apiVersion, + conversationUser.baseUrl, + conversationToken + ), + intValue + ) + messageNotificationLevel = value + } + } else { + messageNotificationLevel = value + } + } + } catch (exception: Exception) { + Log.e(TAG, "Error trying to set notification level", exception) + } + } + else -> { + arbitraryStorageManager!!.storeStorageSetting(accountIdentifier, key, value, conversationToken) + } + } + } + + fun getBoolean(key: String, defaultVal: Boolean): Boolean = + if ("lobby_switch" == key) { + lobbyValue + } else { + arbitraryStorageManager!! + .getStorageSetting(accountIdentifier, key, conversationToken) + .map { arbitraryStorage: ArbitraryStorage -> arbitraryStorage.value.toBoolean() } + .blockingGet(defaultVal) + } + + fun getString(key: String, defaultVal: String): String? = + if ("conversation_settings_dropdown" == key) { + when (messageExpiration) { + EXPIRE_4_WEEKS -> "expire_2419200" + EXPIRE_7_DAYS -> "expire_604800" + EXPIRE_1_DAY -> "expire_86400" + EXPIRE_8_HOURS -> "expire_28800" + EXPIRE_1_HOUR -> "expire_3600" + else -> "expire_0" + } + } else if ("conversation_info_message_notifications_dropdown" == key) { + messageNotificationLevel + } else { + arbitraryStorageManager!! + .getStorageSetting(accountIdentifier, key, conversationToken) + .map(ArbitraryStorage::value) + .blockingGet(defaultVal) + } + + fun setMessageExpiration(messageExpiration: Int) { + this.messageExpiration = messageExpiration + } + + companion object { + private const val TAG = "DatabaseStorageModule" + private const val EXPIRE_1_HOUR = 3600 + private const val EXPIRE_8_HOURS = 28800 + private const val EXPIRE_1_DAY = 86400 + private const val EXPIRE_7_DAYS = 604800 + private const val EXPIRE_4_WEEKS = 2419200 + private const val NOTIFICATION_NEVER = 3 + private const val NOTIFICATION_MENTION = 2 + private const val NOTIFICATION_ALWAYS = 1 + private const val API_VERSION_4 = 4 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtils.kt new file mode 100644 index 0000000..c2924d5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtils.kt @@ -0,0 +1,194 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.utils.preview + +import android.content.Context +import com.github.aurae.retrofit2.LoganSquareConverterFactory +import com.nextcloud.android.common.ui.color.ColorUtil +import com.nextcloud.android.common.ui.theme.MaterialSchemes +import com.nextcloud.android.common.ui.theme.utils.AndroidViewThemeUtils +import com.nextcloud.android.common.ui.theme.utils.AndroidXViewThemeUtils +import com.nextcloud.android.common.ui.theme.utils.DialogViewThemeUtils +import com.nextcloud.android.common.ui.theme.utils.MaterialViewThemeUtils +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.api.NcApiCoroutines +import com.nextcloud.talk.chat.data.ChatMessageRepository +import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager +import com.nextcloud.talk.chat.data.io.MediaRecorderManager +import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource +import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository +import com.nextcloud.talk.chat.data.network.RetrofitChatNetwork +import com.nextcloud.talk.chat.viewmodels.ChatViewModel +import com.nextcloud.talk.contacts.ContactsRepository +import com.nextcloud.talk.contacts.ContactsRepositoryImpl +import com.nextcloud.talk.contacts.ContactsViewModel +import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository +import com.nextcloud.talk.conversationlist.data.network.ConversationsNetworkDataSource +import com.nextcloud.talk.conversationlist.data.network.OfflineFirstConversationsRepository +import com.nextcloud.talk.conversationlist.data.network.RetrofitConversationsNetwork +import com.nextcloud.talk.data.database.dao.ChatBlocksDao +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.database.dao.ConversationsDao +import com.nextcloud.talk.data.network.NetworkMonitor +import com.nextcloud.talk.data.network.NetworkMonitorImpl +import com.nextcloud.talk.data.user.UsersDao +import com.nextcloud.talk.data.user.UsersRepository +import com.nextcloud.talk.data.user.UsersRepositoryImpl +import com.nextcloud.talk.repositories.reactions.ReactionsRepository +import com.nextcloud.talk.repositories.reactions.ReactionsRepositoryImpl +import com.nextcloud.talk.threadsoverview.data.ThreadsRepository +import com.nextcloud.talk.threadsoverview.data.ThreadsRepositoryImpl +import com.nextcloud.talk.ui.theme.MaterialSchemesProviderImpl +import com.nextcloud.talk.ui.theme.TalkSpecificViewThemeUtils +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.database.user.CurrentUserProviderImpl +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import com.nextcloud.talk.utils.message.MessageUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.nextcloud.talk.utils.preferences.AppPreferencesImpl +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory + +/** + * TODO - basically a reimplementation of common dependencies for use in Previewing Advanced Compose Views + * It's a hard coded Dependency Injector + * + */ +class ComposePreviewUtils private constructor(context: Context) { + private val mContext = context + + companion object { + fun getInstance(context: Context) = ComposePreviewUtils(context) + val TAG: String = ComposePreviewUtils::class.java.simpleName + } + + @OptIn(ExperimentalCoroutinesApi::class) + val appPreferences: AppPreferences + get() = AppPreferencesImpl(mContext) + + val context: Context = mContext + + val userRepository: UsersRepository + get() = UsersRepositoryImpl(usersDao) + + val userManager: UserManager + get() = UserManager(userRepository) + + val userProvider: CurrentUserProviderNew + get() = CurrentUserProviderImpl(userManager) + + val colorUtil: ColorUtil + get() = ColorUtil(mContext) + + val materialScheme: MaterialSchemes + get() = MaterialSchemesProviderImpl(userProvider, colorUtil).getMaterialSchemesForCurrentUser() + + val viewThemeUtils: ViewThemeUtils + get() { + val android = AndroidViewThemeUtils(materialScheme, colorUtil) + val material = MaterialViewThemeUtils(materialScheme, colorUtil) + val androidx = AndroidXViewThemeUtils(materialScheme, android) + val talk = TalkSpecificViewThemeUtils(materialScheme, androidx) + val dialog = DialogViewThemeUtils(materialScheme) + return ViewThemeUtils(materialScheme, android, material, androidx, talk, dialog) + } + + val messageUtils: MessageUtils + get() = MessageUtils(mContext) + + val retrofit: Retrofit + get() { + val retrofitBuilder = Retrofit.Builder() + .client(OkHttpClient.Builder().build()) + .baseUrl("https://nextcloud.com") + .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) + .addConverterFactory(LoganSquareConverterFactory.create()) + + return retrofitBuilder.build() + } + + val ncApi: NcApi + get() = retrofit.create(NcApi::class.java) + + val ncApiCoroutines: NcApiCoroutines + get() = retrofit.create(NcApiCoroutines::class.java) + + val chatNetworkDataSource: ChatNetworkDataSource + get() = RetrofitChatNetwork(ncApi, ncApiCoroutines) + + val usersDao: UsersDao + get() = DummyUserDaoImpl() + + val chatMessagesDao: ChatMessagesDao + get() = DummyChatMessagesDaoImpl() + + val chatBlocksDao: ChatBlocksDao + get() = DummyChatBlocksDaoImpl() + + val conversationsDao: ConversationsDao + get() = DummyConversationDaoImpl() + + val networkMonitor: NetworkMonitor + get() = NetworkMonitorImpl(mContext) + + val chatRepository: ChatMessageRepository + get() = OfflineFirstChatRepository( + chatMessagesDao, + chatBlocksDao, + chatNetworkDataSource, + networkMonitor, + userProvider + ) + + val threadsRepository: ThreadsRepository + get() = ThreadsRepositoryImpl(ncApiCoroutines, userProvider) + + val conversationNetworkDataSource: ConversationsNetworkDataSource + get() = RetrofitConversationsNetwork(ncApi) + + val conversationRepository: OfflineConversationsRepository + get() = OfflineFirstConversationsRepository( + conversationsDao, + conversationNetworkDataSource, + chatNetworkDataSource, + networkMonitor, + userProvider + ) + + val reactionsRepository: ReactionsRepository + get() = ReactionsRepositoryImpl(ncApi, userProvider, chatMessagesDao) + + val mediaRecorderManager: MediaRecorderManager + get() = MediaRecorderManager() + + val audioFocusRequestManager: AudioFocusRequestManager + get() = AudioFocusRequestManager(mContext) + + val chatViewModel: ChatViewModel + get() = ChatViewModel( + appPreferences, + chatNetworkDataSource, + chatRepository, + threadsRepository, + conversationRepository, + reactionsRepository, + mediaRecorderManager, + audioFocusRequestManager, + userProvider + ) + + val contactsRepository: ContactsRepository + get() = ContactsRepositoryImpl(ncApiCoroutines, userProvider) + + val contactsViewModel: ContactsViewModel + get() = ContactsViewModel(contactsRepository) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt new file mode 100644 index 0000000..5743d04 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/preview/ComposePreviewUtilsDaos.kt @@ -0,0 +1,229 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.utils.preview + +import com.nextcloud.talk.data.database.dao.ChatBlocksDao +import com.nextcloud.talk.data.database.dao.ChatMessagesDao +import com.nextcloud.talk.data.database.dao.ConversationsDao +import com.nextcloud.talk.data.database.model.ChatBlockEntity +import com.nextcloud.talk.data.database.model.ChatMessageEntity +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.data.user.UsersDao +import com.nextcloud.talk.data.user.model.UserEntity +import com.nextcloud.talk.models.json.push.PushConfigurationState +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class DummyChatMessagesDaoImpl : ChatMessagesDao { + override fun getMessagesForConversation(internalConversationId: String): Flow> = flowOf() + + override fun getTempMessagesForConversation(internalConversationId: String): Flow> = + flowOf() + + override fun getTempUnsentMessagesForConversation( + internalConversationId: String, + threadId: Long? + ): Flow> { + // nothing to return here as long this class is only used for the Search window + return flowOf() + } + + override fun getTempMessageForConversation( + internalConversationId: String, + referenceId: String, + threadId: Long? + ): Flow = flowOf() + + override suspend fun upsertChatMessages(chatMessages: List) { /* */ } + + override suspend fun upsertChatMessage(chatMessage: ChatMessageEntity) { /* */ } + + override fun getChatMessageForConversation( + internalConversationId: String, + messageId: Long + ): Flow = flowOf() + + override fun deleteChatMessages(internalIds: List) { /* */ } + + override fun deleteTempChatMessages(internalConversationId: String, referenceIds: List) { /* */ } + + override fun updateChatMessage(message: ChatMessageEntity) { /* */ } + + override fun getMessagesFromIds(messageIds: List): Flow> = flowOf() + + override fun getMessagesForConversationSince( + internalConversationId: String, + messageId: Long, + threadId: Long? + ): Flow> = flowOf() + + override fun getMessagesForConversationBefore( + internalConversationId: String, + messageId: Long, + limit: Int, + threadId: Long? + ): Flow> = flowOf() + + override fun getMessagesForConversationBeforeAndEqual( + internalConversationId: String, + messageId: Long, + limit: Int, + threadId: Long? + ): Flow> = flowOf() + + override fun getCountBetweenMessageIds( + internalConversationId: String, + oldestMessageId: Long, + newestMessageId: Long, + threadId: Long? + ): Int = 0 + + override fun clearAllMessagesForUser(pattern: String) { /* */ } + + override fun deleteMessagesOlderThan(internalConversationId: String, messageId: Long) { /* */ } + + override fun getNumberOfThreadReplies(internalConversationId: String, threadId: Long): Int = 0 +} + +class DummyUserDaoImpl : UsersDao() { + private val dummyUsers = mutableListOf( + UserEntity(1L, "user1_id", "user1", "server1", "1"), + UserEntity(2L, "user2_id", "user2", "server1", "2"), + UserEntity(0L, "user3_id", "user3", "server2", "3") + ) + private var activeUserId: Long? = 1L + + override fun getActiveUser(): Maybe = + Maybe.fromCallable { + dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } + } + + override fun getActiveUserObservable(): Observable = + Observable.fromCallable { + dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } + } + + override fun getActiveUserSynchronously(): UserEntity? = + dummyUsers.find { + it.id == activeUserId && !it.scheduledForDeletion + } + + override fun deleteUser(user: UserEntity): Int { + val initialSize = dummyUsers.size + dummyUsers.removeIf { it.id == user.id } + return initialSize - dummyUsers.size + } + + override fun updateUser(user: UserEntity): Int { + val index = dummyUsers.indexOfFirst { it.id == user.id } + return if (index != -1) { + dummyUsers[index] = user + 1 + } else { + 0 + } + } + + override fun saveUser(user: UserEntity): Long { + val newUser = user.copy(id = dummyUsers.size + 1L) + dummyUsers.add(newUser) + return newUser.id + } + + override fun saveUsers(vararg users: UserEntity): List = users.map { saveUser(it) } + + override fun getUsers(): Single> = Single.just(dummyUsers.filter { !it.scheduledForDeletion }) + + override fun getUserWithId(id: Long): Maybe = Maybe.fromCallable { dummyUsers.find { it.id == id } } + + override fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe = + Maybe.fromCallable { + dummyUsers.find { it.id == id && !it.scheduledForDeletion } + } + + override fun getUserWithUserId(userId: String): Maybe = + Maybe.fromCallable { + dummyUsers.find { it.userId == userId } + } + + override fun getUsersScheduledForDeletion(): Single> = + Single.just( + dummyUsers.filter { + it.scheduledForDeletion + } + ) + + override fun getUsersNotScheduledForDeletion(): Single> = + Single.just( + dummyUsers.filter { + !it.scheduledForDeletion + } + ) + + override fun getUserWithUsernameAndServer(username: String, server: String): Maybe = + Maybe.fromCallable { + dummyUsers.find { it.username == username } + } + + override fun setUserAsActiveWithId(id: Long): Int { + activeUserId = id + return 1 + } + + override fun updatePushState(id: Long, state: PushConfigurationState): Single { + val index = dummyUsers.indexOfFirst { it.id == id } + return if (index != -1) { + dummyUsers[index] = dummyUsers[index] + Single.just(1) + } else { + Single.just(0) + } + } +} + +class DummyConversationDaoImpl : ConversationsDao { + override fun getConversationsForUser(accountId: Long): Flow> = flowOf() + + override fun getConversationForUser(accountId: Long, token: String): Flow = flowOf() + + override suspend fun upsertConversations(accountId: Long, serverItems: List) { /* */ } + + override fun deleteConversations(conversationIds: List) { /* */ } + + override fun updateConversation(conversationEntity: ConversationEntity) { /* */ } + + override fun insertConversation(conversation: ConversationEntity) { /* */ } + + override fun clearAllConversationsForUser(accountId: Long) { /* */ } +} + +class DummyChatBlocksDaoImpl : ChatBlocksDao { + override fun deleteChatBlocks(blocks: List) { /* */ } + + override fun getChatBlocksContainingMessageId( + internalConversationId: String, + threadId: Long?, + messageId: Long + ): Flow> = flowOf() + + override fun getConnectedChatBlocks( + internalConversationId: String, + threadId: Long?, + oldestMessageId: Long, + newestMessageId: Long + ): Flow> = flowOf() + + override fun getNewestMessageIdFromChatBlocks(internalConversationId: String, threadId: Long?): Long = 0L + + override suspend fun upsertChatBlock(chatBlock: ChatBlockEntity) { /* */ } + + override fun deleteChatBlocksOlderThan(internalConversationId: String, messageId: Long) { /* */ } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/rx/DisposableSet.kt b/app/src/main/java/com/nextcloud/talk/utils/rx/DisposableSet.kt new file mode 100644 index 0000000..1d70012 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/rx/DisposableSet.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.rx + +import io.reactivex.disposables.Disposable + +class DisposableSet { + private val disposables = mutableSetOf() + + fun add(disposable: Disposable) { + disposables.add(disposable) + } + + fun dispose() { + disposables.forEach { it.dispose() } + disposables.clear() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/rx/SearchViewObservable.kt b/app/src/main/java/com/nextcloud/talk/utils/rx/SearchViewObservable.kt new file mode 100644 index 0000000..2f13957 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/rx/SearchViewObservable.kt @@ -0,0 +1,34 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.rx + +import androidx.appcompat.widget.SearchView +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject + +class SearchViewObservable { + + companion object { + @JvmStatic + fun observeSearchView(searchView: SearchView): Observable { + val subject: PublishSubject = PublishSubject.create() + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + subject.onComplete() + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + subject.onNext(newText) + return true + } + }) + return subject + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java b/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java new file mode 100644 index 0000000..515bae0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java @@ -0,0 +1,95 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.singletons; + +import android.util.Log; + +import com.nextcloud.talk.data.user.model.User; + +public class ApplicationWideCurrentRoomHolder { + + public static final String TAG = "ApplicationWideCurrentRoomHolder"; + private static final ApplicationWideCurrentRoomHolder holder = new ApplicationWideCurrentRoomHolder(); +// private String currentRoomId = ""; + private String currentRoomToken = ""; + private User userInRoom = new User(); + private boolean inCall = false; + private boolean isDialing = false; + private String session = ""; + + private Long callStartTime = null; + + public static ApplicationWideCurrentRoomHolder getInstance() { + return holder; + } + + public void clear() { + Log.d(TAG, "ApplicationWideCurrentRoomHolder was cleared"); +// currentRoomId = ""; + userInRoom = new User(); + inCall = false; + isDialing = false; + currentRoomToken = ""; + session = ""; + } + + public String getCurrentRoomToken() { + return currentRoomToken; + } + + public void setCurrentRoomToken(String currentRoomToken) { + this.currentRoomToken = currentRoomToken; + } + +// public String getCurrentRoomId() { +// return currentRoomId; +// } +// +// public void setCurrentRoomId(String currentRoomId) { +// this.currentRoomId = currentRoomId; +// } + + public User getUserInRoom() { + return userInRoom; + } + + public void setUserInRoom(User userInRoom) { + this.userInRoom = userInRoom; + } + + public boolean isInCall() { + return inCall; + } + + public void setInCall(boolean inCall) { + this.inCall = inCall; + } + + public boolean isDialing() { + return isDialing; + } + + public void setDialing(boolean dialing) { + isDialing = dialing; + } + + public String getSession() { + return session; + } + + public void setSession(String session) { + this.session = session; + } + + public Long getCallStartTime() { + return callStartTime; + } + + public void setCallStartTime(Long callStartTime) { + this.callStartTime = callStartTime; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideMessageHolder.java b/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideMessageHolder.java new file mode 100644 index 0000000..fb84293 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideMessageHolder.java @@ -0,0 +1,33 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.singletons; + +import androidx.annotation.Nullable; + +public class ApplicationWideMessageHolder { + private static final ApplicationWideMessageHolder holder = new ApplicationWideMessageHolder(); + private MessageType messageType; + + public static ApplicationWideMessageHolder getInstance() { + return holder; + } + + public MessageType getMessageType() { + return messageType; + } + + public void setMessageType(@Nullable MessageType messageType) { + this.messageType = messageType; + } + + public enum MessageType { + WRONG_ACCOUNT, ACCOUNT_UPDATED_NOT_ADDED, SERVER_WITHOUT_TALK, + FAILED_TO_IMPORT_ACCOUNT, ACCOUNT_WAS_IMPORTED, CALL_PASSWORD_WRONG + } + + +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/singletons/AvatarStatusCodeHolder.java b/app/src/main/java/com/nextcloud/talk/utils/singletons/AvatarStatusCodeHolder.java new file mode 100644 index 0000000..5dc85dd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/singletons/AvatarStatusCodeHolder.java @@ -0,0 +1,24 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.singletons; + +public class AvatarStatusCodeHolder { + private static final AvatarStatusCodeHolder holder = new AvatarStatusCodeHolder(); + private int statusCode; + + public static AvatarStatusCodeHolder getInstance() { + return holder; + } + + public int getStatusCode() { + return statusCode; + } + + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/KeyManager.java b/app/src/main/java/com/nextcloud/talk/utils/ssl/KeyManager.java new file mode 100644 index 0000000..26c3051 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ssl/KeyManager.java @@ -0,0 +1,191 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.ssl; + +import android.content.Context; +import android.security.KeyChain; +import android.security.KeyChainException; +import android.text.TextUtils; +import android.util.Log; + +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.users.UserManager; +import com.nextcloud.talk.utils.preferences.AppPreferences; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.X509KeyManager; + +import androidx.annotation.Nullable; + +public class KeyManager implements X509KeyManager { + private static final String TAG = "KeyManager"; + private final X509KeyManager keyManager; + + private UserManager userManager; + private AppPreferences appPreferences; + private Context context; + + public KeyManager(X509KeyManager keyManager, UserManager userManager, AppPreferences appPreferences) { + this.keyManager = keyManager; + this.userManager = userManager; + this.appPreferences = appPreferences; + + context = NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext(); + } + + @Override + public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { + String alias; + User currentUser = userManager.getCurrentUser().blockingGet(); + if ((currentUser != null && + !TextUtils.isEmpty(alias = currentUser.getClientCertificate())) || + !TextUtils.isEmpty(alias = appPreferences.getTemporaryClientCertAlias()) + && new ArrayList<>(Arrays.asList(getClientAliases())).contains(alias)) { + return alias; + } + + return null; + } + + @Override + public String chooseServerAlias(String s, Principal[] principals, Socket socket) { + return null; + } + + private X509Certificate[] getCertificatesForAlias(@Nullable String alias) { + if (alias != null) { + GetCertificatesForAliasRunnable getCertificatesForAliasRunnable = new GetCertificatesForAliasRunnable(alias); + Thread getCertificatesThread = new Thread(getCertificatesForAliasRunnable); + getCertificatesThread.start(); + try { + getCertificatesThread.join(); + return getCertificatesForAliasRunnable.getCertificates(); + } catch (InterruptedException e) { + Log.e(TAG, "Failed to join the thread while getting certificates: " + e.getLocalizedMessage()); + } + } + + return null; + } + + private PrivateKey getPrivateKeyForAlias(@Nullable String alias) { + if (alias != null) { + GetPrivateKeyForAliasRunnable getPrivateKeyForAliasRunnable = new GetPrivateKeyForAliasRunnable(alias); + Thread getPrivateKeyThread = new Thread(getPrivateKeyForAliasRunnable); + getPrivateKeyThread.start(); + try { + getPrivateKeyThread.join(); + return getPrivateKeyForAliasRunnable.getPrivateKey(); + } catch (InterruptedException e) { + Log.e(TAG, "Failed to join the thread while getting private key: " + e.getLocalizedMessage()); + } + } + + return null; + } + + + @Override + public X509Certificate[] getCertificateChain(String s) { + if (new ArrayList<>(Arrays.asList(getClientAliases())).contains(s)) { + return getCertificatesForAlias(s); + } + + return null; + } + + private String[] getClientAliases() { + Set aliases = new HashSet<>(); + String alias; + if (!TextUtils.isEmpty(alias = appPreferences.getTemporaryClientCertAlias())) { + aliases.add(alias); + } + + List userEntities = userManager.getUsers().blockingGet(); + for (int i = 0; i < userEntities.size(); i++) { + if (!TextUtils.isEmpty(alias = userEntities.get(i).getClientCertificate())) { + aliases.add(alias); + } + } + + return aliases.toArray(new String[aliases.size()]); + } + + @Override + public String[] getClientAliases(String s, Principal[] principals) { + return getClientAliases(); + } + + @Override + public String[] getServerAliases(String s, Principal[] principals) { + return null; + } + + @Override + public PrivateKey getPrivateKey(String s) { + if (new ArrayList<>(Arrays.asList(getClientAliases())).contains(s)) { + return getPrivateKeyForAlias(s); + } + + return null; + } + + private class GetCertificatesForAliasRunnable implements Runnable { + private volatile X509Certificate[] certificates; + private String alias; + + public GetCertificatesForAliasRunnable(String alias) { + this.alias = alias; + } + + @Override + public void run() { + try { + certificates = KeyChain.getCertificateChain(context, alias); + } catch (KeyChainException | InterruptedException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + } + + public X509Certificate[] getCertificates() { + return certificates; + } + } + + private class GetPrivateKeyForAliasRunnable implements Runnable { + private volatile PrivateKey privateKey; + private String alias; + + public GetPrivateKeyForAliasRunnable(String alias) { + this.alias = alias; + } + + @Override + public void run() { + try { + privateKey = KeyChain.getPrivateKey(context, alias); + } catch (KeyChainException | InterruptedException e) { + Log.e(TAG, e.getLocalizedMessage()); + } + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + } + +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt b/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt new file mode 100644 index 0000000..76b8b2b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ssl/SSLSocketFactoryCompat.kt @@ -0,0 +1,101 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2024 Ricki Hirner (bitfire web engineering) + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.ssl + +import java.net.InetAddress +import java.net.Socket +import java.security.GeneralSecurityException +import javax.net.ssl.KeyManager +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager + +class SSLSocketFactoryCompat(keyManager: KeyManager?, trustManager: X509TrustManager) : SSLSocketFactory() { + + private var delegate: SSLSocketFactory + + companion object { + // Android 5.0+ (API level 21) provides reasonable default settings + // but it still allows SSLv3 + // https://developer.android.com/reference/javax/net/ssl/SSLSocket.html + var protocols: Array? = null + var cipherSuites: Array? = null + + init { + // Since Android 6.0 (API level 23), + // - TLSv1.1 and TLSv1.2 is enabled by default + // - SSLv3 is disabled by default + // - all modern ciphers are activated by default + protocols = null + cipherSuites = null + } + } + + init { + try { + val sslContext = SSLContext.getInstance("TLS") + sslContext.init( + if (keyManager != null) arrayOf(keyManager) else null, + arrayOf(trustManager), + null + ) + delegate = sslContext.socketFactory + } catch (e: GeneralSecurityException) { + throw IllegalStateException() // system has no TLS + } + } + + override fun getDefaultCipherSuites(): Array? = cipherSuites ?: delegate.defaultCipherSuites + + override fun getSupportedCipherSuites(): Array? = cipherSuites ?: delegate.supportedCipherSuites + + override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket { + val ssl = delegate.createSocket(s, host, port, autoClose) + if (ssl is SSLSocket) { + upgradeTLS(ssl) + } + return ssl + } + + override fun createSocket(host: String, port: Int): Socket { + val ssl = delegate.createSocket(host, port) + if (ssl is SSLSocket) { + upgradeTLS(ssl) + } + return ssl + } + + override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket { + val ssl = delegate.createSocket(host, port, localHost, localPort) + if (ssl is SSLSocket) { + upgradeTLS(ssl) + } + return ssl + } + + override fun createSocket(host: InetAddress, port: Int): Socket { + val ssl = delegate.createSocket(host, port) + if (ssl is SSLSocket) { + upgradeTLS(ssl) + } + return ssl + } + + override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket { + val ssl = delegate.createSocket(address, port, localAddress, localPort) + if (ssl is SSLSocket) { + upgradeTLS(ssl) + } + return ssl + } + + private fun upgradeTLS(ssl: SSLSocket) { + protocols?.let { ssl.enabledProtocols = it } + cipherSuites?.let { ssl.enabledCipherSuites = it } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ssl/TrustManager.java b/app/src/main/java/com/nextcloud/talk/utils/ssl/TrustManager.java new file mode 100644 index 0000000..1cb96ac --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ssl/TrustManager.java @@ -0,0 +1,183 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-FileCopyrightText: 2017 Ricki Hirner + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Influenced by https://gitlab.com/bitfireAT/cert4android/blob/master/src/main/java/at/bitfire/cert4android/CustomCertService.kt + */ + +package com.nextcloud.talk.utils.ssl; + +import android.content.Context; +import android.util.Log; + +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.events.CertificateEvent; + +import org.greenrobot.eventbus.EventBus; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + + +public class TrustManager implements X509TrustManager { + private static final String TAG = "TrustManager"; + + private final File keystoreFile; + private X509TrustManager systemTrustManager = null; + private KeyStore trustedKeyStore = null; + + public TrustManager() { + keystoreFile = new File(NextcloudTalkApplication.Companion.getSharedApplication() + .getDir("CertsKeystore", Context.MODE_PRIVATE), + "keystore.bks"); + try { + trustedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + } catch (KeyStoreException e) { + Log.e(TAG, "Trusted key store can't be created.", e); + } + + if (keystoreFile.exists()) { + try (FileInputStream fileInputStream = new FileInputStream(keystoreFile)) { + trustedKeyStore.load(fileInputStream, null); + } catch (Exception exception) { + Log.e(TAG, "Error during opening the trusted key store.", exception); + } + } else { + try { + trustedKeyStore.load(null, null); + } catch (Exception e) { + Log.d(TAG, "Failed to create in-memory key store " + e.getLocalizedMessage()); + } + } + + TrustManagerFactory trustManagerFactory = null; + try { + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory. + getDefaultAlgorithm()); + + trustManagerFactory.init((KeyStore) null); + + for (javax.net.ssl.TrustManager trustManager : trustManagerFactory.getTrustManagers()) { + if (trustManager instanceof X509TrustManager) { + systemTrustManager = (X509TrustManager) trustManager; + break; + } + } + + } catch (Exception exception) { + Log.d(TAG, "Failed to load default trust manager " + exception.getLocalizedMessage()); + } + + } + + public javax.net.ssl.HostnameVerifier getHostnameVerifier(javax.net.ssl.HostnameVerifier defaultHostNameVerifier) { + return new HostnameVerifier(defaultHostNameVerifier); + } + + private boolean isCertInTrustStore(X509Certificate[] x509Certificates, String s) { + if (systemTrustManager != null) { + X509Certificate x509Certificate = x509Certificates[0]; + try { + systemTrustManager.checkServerTrusted(x509Certificates, s); + return true; + } catch (CertificateException e) { + if (!isCertInTrustStore(x509Certificate)) { + EventBus.getDefault().post(new CertificateEvent(x509Certificate, this, + null)); + long startTime = System.currentTimeMillis(); + while (!isCertInTrustStore(x509Certificate) && System.currentTimeMillis() <= + startTime + 15000) { + //do nothing + } + return isCertInTrustStore(x509Certificate); + } else { + return true; + } + } + } + + return false; + } + + private boolean isCertInTrustStore(X509Certificate x509Certificate) { + if (trustedKeyStore != null) { + try { + if (trustedKeyStore.getCertificateAlias(x509Certificate) != null) { + return true; + } + } catch (KeyStoreException exception) { + return false; + } + } + + return false; + } + + public void addCertInTrustStore(X509Certificate x509Certificate) { + if (trustedKeyStore != null) { + try (FileOutputStream fileOutputStream = new FileOutputStream(keystoreFile)) { + trustedKeyStore.setCertificateEntry(x509Certificate.getSubjectDN().getName(), x509Certificate); + trustedKeyStore.store(fileOutputStream, null); + } catch (Exception exception) { + Log.d(TAG, "Failed to set certificate entry " + exception.getLocalizedMessage()); + } + } + } + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + systemTrustManager.checkClientTrusted(x509Certificates, s); + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + if (!isCertInTrustStore(x509Certificates, s)) { + throw new CertificateException(); + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return systemTrustManager.getAcceptedIssuers(); + } + + private class HostnameVerifier implements javax.net.ssl.HostnameVerifier { + private static final String TAG = "HostnameVerifier"; + private final javax.net.ssl.HostnameVerifier defaultHostNameVerifier; + + private HostnameVerifier(javax.net.ssl.HostnameVerifier defaultHostNameVerifier) { + this.defaultHostNameVerifier = defaultHostNameVerifier; + } + + @Override + public boolean verify(String s, SSLSession sslSession) { + + if (defaultHostNameVerifier.verify(s, sslSession)) { + try { + X509Certificate[] certificates = (X509Certificate[]) sslSession.getPeerCertificates(); + if (certificates.length > 0 && isCertInTrustStore(certificates, s)) { + return true; + } + } catch (SSLPeerUnverifiedException e) { + Log.d(TAG, "Couldn't get certificate for host name verification"); + } + } + + return false; + } + } + +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/text/Spans.java b/app/src/main/java/com/nextcloud/talk/utils/text/Spans.java new file mode 100644 index 0000000..1c35eff --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/text/Spans.java @@ -0,0 +1,84 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils.text; + +import android.graphics.drawable.Drawable; + + + +import androidx.annotation.NonNull; +import third.parties.fresco.BetterImageSpan; + +public class Spans { + + public static class MentionChipSpan extends BetterImageSpan { + public String id; + public CharSequence label; + + public MentionChipSpan(@NonNull Drawable drawable, int verticalAlignment, String id, CharSequence label) { + super(drawable, verticalAlignment); + this.id = id; + this.label = label; + } + + public String getId() { + return this.id; + } + + public CharSequence getLabel() { + return this.label; + } + + public void setId(String id) { + this.id = id; + } + + public void setLabel(CharSequence label) { + this.label = label; + } + + public boolean equals(final Object o) { + if (o == this) { + return true; + } + if (!(o instanceof MentionChipSpan)) { + return false; + } + final MentionChipSpan other = (MentionChipSpan) o; + if (!other.canEqual((Object) this)) { + return false; + } + final Object this$id = this.getId(); + final Object other$id = other.getId(); + if (this$id == null ? other$id != null : !this$id.equals(other$id)) { + return false; + } + final Object this$label = this.getLabel(); + final Object other$label = other.getLabel(); + + return this$label == null ? other$label == null : this$label.equals(other$label); + } + + protected boolean canEqual(final Object other) { + return other instanceof MentionChipSpan; + } + + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $id = this.getId(); + result = result * PRIME + ($id == null ? 43 : $id.hashCode()); + final Object $label = this.getLabel(); + return result * PRIME + ($label == null ? 43 : $label.hashCode()); + } + + public String toString() { + return "Spans.MentionChipSpan(id=" + this.getId() + ", label=" + this.getLabel() + ")"; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt b/app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt new file mode 100644 index 0000000..c8fc964 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt @@ -0,0 +1,159 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.models.domain.StartCallRecordingModel +import com.nextcloud.talk.models.domain.StopCallRecordingModel +import com.nextcloud.talk.repositories.callrecording.CallRecordingRepository +import com.nextcloud.talk.users.UserManager +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class CallRecordingViewModel @Inject constructor(private val repository: CallRecordingRepository) : ViewModel() { + + @Inject + lateinit var userManager: UserManager + + lateinit var roomToken: String + + sealed interface ViewState + open class RecordingStartedState(val hasVideo: Boolean, val showStartedInfo: Boolean) : ViewState + + object RecordingStoppedState : ViewState + open class RecordingStartingState(val hasVideo: Boolean) : ViewState + object RecordingStoppingState : ViewState + object RecordingConfirmStopState : ViewState + object RecordingErrorState : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(RecordingStoppedState) + val viewState: LiveData + get() = _viewState + + private var disposable: Disposable? = null + + fun clickRecordButton() { + when (viewState.value) { + is RecordingStartedState -> { + _viewState.value = RecordingConfirmStopState + } + RecordingStoppedState -> { + startRecording() + } + RecordingConfirmStopState -> { + // confirm dialog to stop recording might have been dismissed without to click an action. + // just show it again. + _viewState.value = RecordingConfirmStopState + } + is RecordingStartingState -> { + stopRecording() + } + RecordingErrorState -> { + stopRecording() + } + else -> {} + } + } + + private fun startRecording() { + _viewState.value = RecordingStartingState(true) + repository.startRecording(roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(CallStartRecordingObserver()) + } + + fun stopRecording() { + _viewState.value = RecordingStoppingState + repository.stopRecording(roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(CallStopRecordingObserver()) + } + + fun dismissStopRecording() { + _viewState.value = RecordingStartedState(true, false) + } + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + fun setData(roomToken: String) { + this.roomToken = roomToken + } + + // https://nextcloud-talk.readthedocs.io/en/latest/constants/#call-recording-status + fun setRecordingState(state: Int) { + when (state) { + RECORDING_STOPPED_CODE -> _viewState.value = RecordingStoppedState + RECORDING_STARTED_VIDEO_CODE -> _viewState.value = RecordingStartedState(true, true) + RECORDING_STARTED_AUDIO_CODE -> _viewState.value = RecordingStartedState(false, true) + RECORDING_STARTING_VIDEO_CODE -> _viewState.value = RecordingStartingState(true) + RECORDING_STARTING_AUDIO_CODE -> _viewState.value = RecordingStartingState(false) + RECORDING_FAILED_CODE -> _viewState.value = RecordingErrorState + else -> {} + } + } + + inner class CallStartRecordingObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(startCallRecordingModel: StartCallRecordingModel) { + // unused atm. RecordingStartedState is set via setRecordingState which is triggered by signaling message. + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failure in CallStartRecordingObserver", e) + _viewState.value = RecordingErrorState + } + + override fun onComplete() { + // dismiss() + } + } + + inner class CallStopRecordingObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(stopCallRecordingModel: StopCallRecordingModel) { + if (stopCallRecordingModel.success) { + _viewState.value = RecordingStoppedState + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failure in CallStopRecordingObserver", e) + _viewState.value = RecordingErrorState + } + + override fun onComplete() { + // dismiss() + } + } + + companion object { + private val TAG = CallRecordingViewModel::class.java.simpleName + const val RECORDING_STOPPED_CODE = 0 + const val RECORDING_STARTED_VIDEO_CODE = 1 + const val RECORDING_STARTED_AUDIO_CODE = 2 + const val RECORDING_STARTING_VIDEO_CODE = 3 + const val RECORDING_STARTING_AUDIO_CODE = 4 + const val RECORDING_FAILED_CODE = 5 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/viewmodels/GeoCodingViewModel.kt b/app/src/main/java/com/nextcloud/talk/viewmodels/GeoCodingViewModel.kt new file mode 100644 index 0000000..ffeb8de --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/viewmodels/GeoCodingViewModel.kt @@ -0,0 +1,64 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Samanwith KSN + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.activities.CallActivity.Companion.TAG +import fr.dudie.nominatim.client.TalkJsonNominatimClient +import fr.dudie.nominatim.model.Address +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import java.io.IOException + +class GeoCodingViewModel : ViewModel() { + private val geocodingResultsLiveData = MutableLiveData>() + private val nominatimClient: TalkJsonNominatimClient + private val okHttpClient: OkHttpClient = OkHttpClient.Builder().build() + private var geocodingResults: List

= ArrayList() + private var query: String = "" + fun getGeocodingResultsLiveData(): LiveData> = geocodingResultsLiveData + + fun getQuery(): String = query + + fun setQuery(query: String) { + this.query = query + } + + fun getGeocodingResults(): List
= geocodingResults + + init { + nominatimClient = TalkJsonNominatimClient( + "https://nominatim.openstreetmap.org/", + okHttpClient, + " android@nextcloud.com" + ) + } + + fun searchLocation() { + if (query.isNotEmpty()) { + CoroutineScope(Dispatchers.IO).launch { + try { + val results = nominatimClient.search(query) as ArrayList
+ for (address in results) { + Log.d(TAG, address.displayName) + Log.d(TAG, address.latitude.toString()) + Log.d(TAG, address.longitude.toString()) + } + geocodingResults = results + geocodingResultsLiveData.postValue(results) + } catch (e: IOException) { + Log.e(TAG, "Failed to get geocoded addresses", e) + } + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/DataChannelMessageNotifier.java b/app/src/main/java/com/nextcloud/talk/webrtc/DataChannelMessageNotifier.java new file mode 100644 index 0000000..2fca22a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/webrtc/DataChannelMessageNotifier.java @@ -0,0 +1,65 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Helper class to register and notify DataChannelMessageListeners. + *

+ * This class is only meant for internal use by PeerConnectionWrapper; listeners must register themselves against + * a PeerConnectionWrapper rather than against a DataChannelMessageNotifier. + */ +public class DataChannelMessageNotifier { + + public final Set dataChannelMessageListeners = + new LinkedHashSet<>(); + + public synchronized void addListener(PeerConnectionWrapper.DataChannelMessageListener listener) { + if (listener == null) { + throw new IllegalArgumentException("DataChannelMessageListener can not be null"); + } + + dataChannelMessageListeners.add(listener); + } + + public synchronized void removeListener(PeerConnectionWrapper.DataChannelMessageListener listener) { + dataChannelMessageListeners.remove(listener); + } + + public synchronized void notifyAudioOn() { + for (PeerConnectionWrapper.DataChannelMessageListener listener : new ArrayList<>(dataChannelMessageListeners)) { + listener.onAudioOn(); + } + } + + public synchronized void notifyAudioOff() { + for (PeerConnectionWrapper.DataChannelMessageListener listener : new ArrayList<>(dataChannelMessageListeners)) { + listener.onAudioOff(); + } + } + + public synchronized void notifyVideoOn() { + for (PeerConnectionWrapper.DataChannelMessageListener listener : new ArrayList<>(dataChannelMessageListeners)) { + listener.onVideoOn(); + } + } + + public synchronized void notifyVideoOff() { + for (PeerConnectionWrapper.DataChannelMessageListener listener : new ArrayList<>(dataChannelMessageListeners)) { + listener.onVideoOff(); + } + } + + public synchronized void notifyNickChanged(String nick) { + for (PeerConnectionWrapper.DataChannelMessageListener listener : new ArrayList<>(dataChannelMessageListeners)) { + listener.onNickChanged(nick); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/Globals.java b/app/src/main/java/com/nextcloud/talk/webrtc/Globals.java new file mode 100644 index 0000000..a658abc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/webrtc/Globals.java @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc; + +public class Globals { + public static final String ROOM_TOKEN = "roomToken"; + + public static final String TARGET_PARTICIPANTS = "participants"; + public static final String TARGET_ROOM = "room"; +} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionNotifier.java b/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionNotifier.java new file mode 100644 index 0000000..cea2a86 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionNotifier.java @@ -0,0 +1,55 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc; + +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Helper class to register and notify PeerConnectionObserver. + *

+ * This class is only meant for internal use by PeerConnectionWrapper; observers must register themselves against + * a PeerConnectionWrapper rather than against a PeerConnectionNotifier. + */ +public class PeerConnectionNotifier { + + private final Set peerConnectionObservers = new LinkedHashSet<>(); + + public synchronized void addObserver(PeerConnectionWrapper.PeerConnectionObserver observer) { + if (observer == null) { + throw new IllegalArgumentException("PeerConnectionObserver can not be null"); + } + + peerConnectionObservers.add(observer); + } + + public synchronized void removeObserver(PeerConnectionWrapper.PeerConnectionObserver observer) { + peerConnectionObservers.remove(observer); + } + + public synchronized void notifyStreamAdded(MediaStream stream) { + for (PeerConnectionWrapper.PeerConnectionObserver observer : new ArrayList<>(peerConnectionObservers)) { + observer.onStreamAdded(stream); + } + } + + public synchronized void notifyStreamRemoved(MediaStream stream) { + for (PeerConnectionWrapper.PeerConnectionObserver observer : new ArrayList<>(peerConnectionObservers)) { + observer.onStreamRemoved(stream); + } + } + + public synchronized void notifyIceConnectionStateChanged(PeerConnection.IceConnectionState state) { + for (PeerConnectionWrapper.PeerConnectionObserver observer : new ArrayList<>(peerConnectionObservers)) { + observer.onIceConnectionStateChanged(state); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java b/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java new file mode 100644 index 0000000..70d59e1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java @@ -0,0 +1,674 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc; + +import android.util.Log; + +import com.bluelinelabs.logansquare.LoganSquare; +import com.nextcloud.talk.models.json.signaling.DataChannelMessage; +import com.nextcloud.talk.models.json.signaling.NCIceCandidate; +import com.nextcloud.talk.models.json.signaling.NCMessagePayload; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; +import com.nextcloud.talk.signaling.SignalingMessageReceiver; +import com.nextcloud.talk.signaling.SignalingMessageSender; + +import org.webrtc.AudioTrack; +import org.webrtc.DataChannel; +import org.webrtc.IceCandidate; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaStream; +import org.webrtc.MediaStreamTrack; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.RtpReceiver; +import org.webrtc.RtpTransceiver; +import org.webrtc.SessionDescription; +import org.webrtc.VideoTrack; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import androidx.annotation.Nullable; + +public class PeerConnectionWrapper { + + private static final String TAG = PeerConnectionWrapper.class.getCanonicalName(); + + private final SignalingMessageReceiver signalingMessageReceiver; + private final WebRtcMessageListener webRtcMessageListener = new WebRtcMessageListener(); + + private final SignalingMessageSender signalingMessageSender; + + private final DataChannelMessageNotifier dataChannelMessageNotifier = new DataChannelMessageNotifier(); + + private final PeerConnectionNotifier peerConnectionNotifier = new PeerConnectionNotifier(); + + private List iceCandidates = new ArrayList<>(); + private PeerConnection peerConnection; + private String sessionId; + private final MediaConstraints mediaConstraints; + private final Map dataChannels = new HashMap<>(); + private final List pendingDataChannelMessages = new ArrayList<>(); + private final SdpObserver sdpObserver; + + private final boolean isMCUPublisher; + private final String videoStreamType; + + // It is assumed that there will be at most one remote stream at each time. + private MediaStream stream; + + /** + * Listener for data channel messages. + *

+ * Messages might have been received on any data channel, independently of its label or whether it was open by the + * local or the remote peer. + *

+ * The messages are bound to a specific peer connection, so each listener is expected to handle messages only for + * a single peer connection. + *

+ * All methods are called on the so called "signaling" thread of WebRTC, which is an internal thread created by the + * WebRTC library and NOT the same thread where signaling messages are received. + */ + public interface DataChannelMessageListener { + void onAudioOn(); + void onAudioOff(); + void onVideoOn(); + void onVideoOff(); + void onNickChanged(String nick); + } + + /** + * Observer for changes on the peer connection. + *

+ * The changes are bound to a specific peer connection, so each observer is expected to handle messages only for + * a single peer connection. + *

+ * All methods are called on the so called "signaling" thread of WebRTC, which is an internal thread created by the + * WebRTC library and NOT the same thread where signaling messages are received. + */ + public interface PeerConnectionObserver { + void onStreamAdded(MediaStream mediaStream); + void onStreamRemoved(MediaStream mediaStream); + void onIceConnectionStateChanged(PeerConnection.IceConnectionState iceConnectionState); + } + + public PeerConnectionWrapper(PeerConnectionFactory peerConnectionFactory, + List iceServerList, + MediaConstraints mediaConstraints, + String sessionId, String localSession, @Nullable MediaStream localStream, + boolean isMCUPublisher, boolean hasMCU, String videoStreamType, + SignalingMessageReceiver signalingMessageReceiver, + SignalingMessageSender signalingMessageSender) { + this.videoStreamType = videoStreamType; + + this.sessionId = sessionId; + this.mediaConstraints = mediaConstraints; + + sdpObserver = new SdpObserver(); + boolean hasInitiated = sessionId.compareTo(localSession) < 0; + this.isMCUPublisher = isMCUPublisher; + + PeerConnection.RTCConfiguration configuration = new PeerConnection.RTCConfiguration(iceServerList); + configuration.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; + peerConnection = peerConnectionFactory.createPeerConnection(configuration, new InitialPeerConnectionObserver()); + + this.signalingMessageReceiver = signalingMessageReceiver; + this.signalingMessageReceiver.addListener(webRtcMessageListener, sessionId, videoStreamType); + + this.signalingMessageSender = signalingMessageSender; + + if (peerConnection != null) { + if (localStream != null) { + List localStreamIds = Collections.singletonList(localStream.getId()); + for(AudioTrack track : localStream.audioTracks) { + peerConnection.addTrack(track, localStreamIds); + } + for(VideoTrack track : localStream.videoTracks) { + peerConnection.addTrack(track, localStreamIds); + } + } + + if (hasMCU || hasInitiated) { + DataChannel.Init init = new DataChannel.Init(); + init.negotiated = false; + + DataChannel statusDataChannel = peerConnection.createDataChannel("status", init); + statusDataChannel.registerObserver(new DataChannelObserver(statusDataChannel)); + dataChannels.put("status", statusDataChannel); + + if (isMCUPublisher) { + peerConnection.createOffer(sdpObserver, mediaConstraints); + } else if (hasMCU && "video".equals(this.videoStreamType)) { + // If the connection type is "screen" the client sharing the screen will send an + // offer; offers should be requested only for videos. + // "to" property is not actually needed in the "requestoffer" signaling message, but it is used to + // set the recipient session ID in the assembled call message. + NCSignalingMessage ncSignalingMessage = createBaseSignalingMessage("requestoffer"); + signalingMessageSender.send(ncSignalingMessage); + } else if (!hasMCU && hasInitiated && "video".equals(this.videoStreamType)) { + // If the connection type is "screen" the client sharing the screen will send an + // offer; offers should be created only for videos. + peerConnection.createOffer(sdpObserver, mediaConstraints); + } + } + } + } + + public void raiseHand(Boolean raise) { + NCMessagePayload ncMessagePayload = new NCMessagePayload(); + ncMessagePayload.setState(raise); + ncMessagePayload.setTimestamp(System.currentTimeMillis()); + + NCSignalingMessage ncSignalingMessage = new NCSignalingMessage(); + ncSignalingMessage.setTo(sessionId); + ncSignalingMessage.setType("raiseHand"); + ncSignalingMessage.setPayload(ncMessagePayload); + ncSignalingMessage.setRoomType(videoStreamType); + + signalingMessageSender.send(ncSignalingMessage); + } + + public void sendReaction(String emoji) { + NCMessagePayload ncMessagePayload = new NCMessagePayload(); + ncMessagePayload.setReaction(emoji); + ncMessagePayload.setTimestamp(System.currentTimeMillis()); + + NCSignalingMessage ncSignalingMessage = new NCSignalingMessage(); + ncSignalingMessage.setTo(sessionId); + ncSignalingMessage.setType("reaction"); + ncSignalingMessage.setPayload(ncMessagePayload); + ncSignalingMessage.setRoomType(videoStreamType); + + signalingMessageSender.send(ncSignalingMessage); + } + + /** + * Adds a listener for data channel messages. + *

+ * A listener is expected to be added only once. If the same listener is added again it will be notified just once. + * + * @param listener the DataChannelMessageListener + */ + public void addListener(DataChannelMessageListener listener) { + dataChannelMessageNotifier.addListener(listener); + } + + public void removeListener(DataChannelMessageListener listener) { + dataChannelMessageNotifier.removeListener(listener); + } + + /** + * Adds an observer for peer connection changes. + *

+ * An observer is expected to be added only once. If the same observer is added again it will be notified just once. + * + * @param observer the PeerConnectionObserver + */ + public void addObserver(PeerConnectionObserver observer) { + peerConnectionNotifier.addObserver(observer); + } + + public void removeObserver(PeerConnectionObserver observer) { + peerConnectionNotifier.removeObserver(observer); + } + + public String getVideoStreamType() { + return videoStreamType; + } + + public MediaStream getStream() { + return stream; + } + + public synchronized void removePeerConnection() { + signalingMessageReceiver.removeListener(webRtcMessageListener); + + for (DataChannel dataChannel: dataChannels.values()) { + Log.d(TAG, "Disposed DataChannel " + dataChannel.label()); + + dataChannel.dispose(); + } + dataChannels.clear(); + + if (peerConnection != null) { + peerConnection.close(); + peerConnection = null; + Log.d(TAG, "Disposed PeerConnection"); + } else { + Log.d(TAG, "PeerConnection is null."); + } + } + + private void drainIceCandidates() { + + if (peerConnection != null) { + for (IceCandidate iceCandidate : iceCandidates) { + peerConnection.addIceCandidate(iceCandidate); + } + + iceCandidates = new ArrayList<>(); + } + } + + private void addCandidate(IceCandidate iceCandidate) { + if (peerConnection != null && peerConnection.getRemoteDescription() != null) { + peerConnection.addIceCandidate(iceCandidate); + } else { + iceCandidates.add(iceCandidate); + } + } + + /** + * Sends a data channel message. + *

+ * Data channel messages are always sent on the "status" data channel locally opened. However, if Janus is used, + * messages can be sent only on publisher connections, even if subscriber connections have a "status" data channel; + * messages sent on subscriber connections will be simply ignored. Moreover, even if the message is sent on the + * "status" data channel subscriber connections will receive it on a data channel with a different label, as + * Janus opens its own data channel on subscriber connections and "multiplexes" all the received data channel + * messages on it, independently of on which data channel they were originally sent. + *

+ * Data channel messages can be sent at any time; if the "status" data channel is not open yet the messages will be + * queued and sent once it is opened. Nevertheless, if Janus is used, it is not guaranteed that the messages will + * be received by other participants, as it is only known when the data channel of the publisher was opened, but + * not if the data channel of the subscribers was. However, in general this should be a concern only during the + * first seconds after a participant joins; after some time the subscriber connections should be established and + * their data channels open. + * + * @param dataChannelMessage the message to send + */ + public synchronized void send(DataChannelMessage dataChannelMessage) { + if (dataChannelMessage == null) { + return; + } + + DataChannel statusDataChannel = dataChannels.get("status"); + if (statusDataChannel == null || statusDataChannel.state() != DataChannel.State.OPEN || + !pendingDataChannelMessages.isEmpty()) { + Log.d(TAG, "Queuing data channel message (" + dataChannelMessage + ") " + sessionId); + + pendingDataChannelMessages.add(dataChannelMessage); + + return; + } + + sendWithoutQueuing(statusDataChannel, dataChannelMessage); + } + + private void sendWithoutQueuing(DataChannel statusDataChannel, DataChannelMessage dataChannelMessage) { + try { + Log.d(TAG, "Sending data channel message (" + dataChannelMessage + ") " + sessionId); + + ByteBuffer buffer = ByteBuffer.wrap(LoganSquare.serialize(dataChannelMessage).getBytes()); + statusDataChannel.send(new DataChannel.Buffer(buffer, false)); + } catch (Exception e) { + Log.w(TAG, "Failed to send data channel message"); + } + } + + public PeerConnection getPeerConnection() { + return peerConnection; + } + + public String getSessionId() { + return sessionId; + } + + public boolean isMCUPublisher() { + return isMCUPublisher; + } + + private boolean shouldNotReceiveVideo() { + for (MediaConstraints.KeyValuePair keyValuePair : mediaConstraints.mandatory) { + if ("OfferToReceiveVideo".equals(keyValuePair.getKey())) { + return !Boolean.parseBoolean(keyValuePair.getValue()); + } + } + return false; + } + + private NCSignalingMessage createBaseSignalingMessage(String type) { + NCSignalingMessage ncSignalingMessage = new NCSignalingMessage(); + ncSignalingMessage.setTo(sessionId); + ncSignalingMessage.setRoomType(videoStreamType); + ncSignalingMessage.setType(type); + + return ncSignalingMessage; + } + + private class WebRtcMessageListener implements SignalingMessageReceiver.WebRtcMessageListener { + + public void onOffer(String sdp, String nick) { + onOfferOrAnswer("offer", sdp); + } + + public void onAnswer(String sdp, String nick) { + onOfferOrAnswer("answer", sdp); + } + + private void onOfferOrAnswer(String type, String sdp) { + SessionDescription sessionDescriptionWithPreferredCodec; + + boolean isAudio = false; + String sessionDescriptionStringWithPreferredCodec = WebRTCUtils.preferCodec(sdp, "H264", isAudio); + + sessionDescriptionWithPreferredCodec = new SessionDescription( + SessionDescription.Type.fromCanonicalForm(type), + sessionDescriptionStringWithPreferredCodec); + + if (getPeerConnection() != null) { + getPeerConnection().setRemoteDescription(sdpObserver, sessionDescriptionWithPreferredCodec); + } + } + + public void onCandidate(String sdpMid, int sdpMLineIndex, String sdp) { + IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, sdp); + addCandidate(iceCandidate); + } + + public void onEndOfCandidates() { + drainIceCandidates(); + } + } + + private class DataChannelObserver implements DataChannel.Observer { + + private final DataChannel dataChannel; + private final String dataChannelLabel; + + public DataChannelObserver(DataChannel dataChannel) { + this.dataChannel = Objects.requireNonNull(dataChannel); + this.dataChannelLabel = dataChannel.label(); + } + + @Override + public void onBufferedAmountChange(long l) { + + } + + @Override + public void onStateChange() { + synchronized (PeerConnectionWrapper.this) { + // The PeerConnection could have been removed in parallel even with the synchronization (as just after + // "onStateChange" was called "removePeerConnection" could have acquired the lock). + if (peerConnection == null) { + return; + } + + if (dataChannel.state() == DataChannel.State.OPEN && "status".equals(dataChannelLabel)) { + for (DataChannelMessage dataChannelMessage : pendingDataChannelMessages) { + sendWithoutQueuing(dataChannel, dataChannelMessage); + } + pendingDataChannelMessages.clear(); + } + } + } + + @Override + public void onMessage(DataChannel.Buffer buffer) { + synchronized (PeerConnectionWrapper.this) { + // It is assumed that, even if its data channel was disposed, its buffers can be used while there is + // a reference to them, so it would not be necessary to check this from a thread-safety point of view. + // Nevertheless, if the remote peer connection was removed it would not make sense to notify the + // listeners anyway. + if (peerConnection == null) { + return; + } + } + + if (buffer.binary) { + Log.d(TAG, "Received binary data channel message over " + dataChannelLabel + " " + sessionId); + return; + } + + ByteBuffer data = buffer.data; + final byte[] bytes = new byte[data.capacity()]; + data.get(bytes); + String strData = new String(bytes); + Log.d(TAG, "Received data channel message (" + strData + ") over " + dataChannelLabel + " " + sessionId); + + DataChannelMessage dataChannelMessage; + try { + dataChannelMessage = LoganSquare.parse(strData, DataChannelMessage.class); + } catch (IOException e) { + Log.d(TAG, "Failed to parse data channel message"); + + return; + } + + if ("nickChanged".equals(dataChannelMessage.getType())) { + String nick = null; + if (dataChannelMessage.getPayload() instanceof String) { + nick = (String) dataChannelMessage.getPayload(); + } else if (dataChannelMessage.getPayload() instanceof Map) { + Map payloadMap = (Map) dataChannelMessage.getPayload(); + nick = payloadMap.get("name"); + } + + if (nick != null) { + dataChannelMessageNotifier.notifyNickChanged(nick); + } + + return; + } + + if ("audioOn".equals(dataChannelMessage.getType())) { + dataChannelMessageNotifier.notifyAudioOn(); + + return; + } + + if ("audioOff".equals(dataChannelMessage.getType())) { + dataChannelMessageNotifier.notifyAudioOff(); + + return; + } + + if ("videoOn".equals(dataChannelMessage.getType())) { + dataChannelMessageNotifier.notifyVideoOn(); + + return; + } + + if ("videoOff".equals(dataChannelMessage.getType())) { + dataChannelMessageNotifier.notifyVideoOff(); + + return; + } + } + } + + private class InitialPeerConnectionObserver implements PeerConnection.Observer { + + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + } + + @Override + public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { + + Log.d("iceConnectionChangeTo: ", iceConnectionState.name() + " over " + peerConnection.hashCode() + " " + sessionId); + + peerConnectionNotifier.notifyIceConnectionStateChanged(iceConnectionState); + } + + @Override + public void onIceConnectionReceivingChange(boolean b) { + + } + + @Override + public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { + + } + + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + NCSignalingMessage ncSignalingMessage = createBaseSignalingMessage("candidate"); + NCMessagePayload ncMessagePayload = new NCMessagePayload(); + ncMessagePayload.setType("candidate"); + + NCIceCandidate ncIceCandidate = new NCIceCandidate(); + ncIceCandidate.setSdpMid(iceCandidate.sdpMid); + ncIceCandidate.setSdpMLineIndex(iceCandidate.sdpMLineIndex); + ncIceCandidate.setCandidate(iceCandidate.sdp); + ncMessagePayload.setIceCandidate(ncIceCandidate); + + ncSignalingMessage.setPayload(ncMessagePayload); + + signalingMessageSender.send(ncSignalingMessage); + } + + @Override + public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { + + } + + @Override + public void onAddStream(MediaStream mediaStream) { + stream = mediaStream; + + peerConnectionNotifier.notifyStreamAdded(mediaStream); + } + + @Override + public void onRemoveStream(MediaStream mediaStream) { + stream = null; + + peerConnectionNotifier.notifyStreamRemoved(mediaStream); + } + + @Override + public void onDataChannel(DataChannel dataChannel) { + synchronized (PeerConnectionWrapper.this) { + // Another data channel with the same label, no matter if the same instance or a different one, should + // not be added, but this is handled just in case. + // Moreover, if it were possible that an already added data channel was added again there would be a + // potential race condition with "removePeerConnection", even with the synchronization, as it would + // be possible that "onDataChannel" was called, then "removePeerConnection" disposed the data + // channel, and then "onDataChannel" continued in the synchronized statements and tried to get the + // label, which would throw an exception due to the data channel having been disposed already. + String dataChannelLabel; + try { + dataChannelLabel = dataChannel.label(); + } catch (IllegalStateException e) { + // The data channel was disposed already, nothing to do. + return; + } + + DataChannel oldDataChannel = dataChannels.get(dataChannelLabel); + if (oldDataChannel == dataChannel) { + Log.w(TAG, "Data channel with label " + dataChannel.label() + " added again"); + + return; + } + + if (oldDataChannel != null) { + Log.w(TAG, "Data channel with label " + dataChannel.label() + " exists"); + + oldDataChannel.dispose(); + } + + // If the peer connection was removed in parallel dispose the data channel instead of adding it. + if (peerConnection == null) { + dataChannel.dispose(); + + return; + } + + dataChannel.registerObserver(new DataChannelObserver(dataChannel)); + dataChannels.put(dataChannel.label(), dataChannel); + } + } + + @Override + public void onRenegotiationNeeded() { + + } + + @Override + public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { + } + } + + private class SdpObserver implements org.webrtc.SdpObserver { + private static final String TAG = "SdpObserver"; + + @Override + public void onCreateFailure(String s) { + Log.d(TAG, "SDPObserver createFailure: " + s + " over " + peerConnection.hashCode() + " " + sessionId); + + } + + @Override + public void onSetFailure(String s) { + Log.d(TAG,"SDPObserver setFailure: " + s + " over " + peerConnection.hashCode() + " " + sessionId); + } + + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + String type = sessionDescription.type.canonicalForm(); + + NCSignalingMessage ncSignalingMessage = createBaseSignalingMessage(type); + NCMessagePayload ncMessagePayload = new NCMessagePayload(); + ncMessagePayload.setType(type); + + SessionDescription sessionDescriptionWithPreferredCodec; + String sessionDescriptionStringWithPreferredCodec = WebRTCUtils.preferCodec + (sessionDescription.description, + "H264", false); + sessionDescriptionWithPreferredCodec = new SessionDescription( + sessionDescription.type, + sessionDescriptionStringWithPreferredCodec); + + ncMessagePayload.setSdp(sessionDescriptionWithPreferredCodec.description); + + ncSignalingMessage.setPayload(ncMessagePayload); + + signalingMessageSender.send(ncSignalingMessage); + + if (peerConnection != null) { + peerConnection.setLocalDescription(sdpObserver, sessionDescriptionWithPreferredCodec); + } + } + + @Override + public void onSetSuccess() { + if (peerConnection != null) { + if (peerConnection.getLocalDescription() == null) { + + if (shouldNotReceiveVideo()) { + for (RtpTransceiver t : peerConnection.getTransceivers()) { + if (t.getMediaType() == MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO) { + t.stop(); + } + } + Log.d(TAG, "Stop all Transceivers for MEDIA_TYPE_VIDEO."); + } + + /* + Passed 'MediaConstraints' will be ignored by WebRTC when using UNIFIED PLAN. + See for details: https://docs.google.com/document/d/1PPHWV6108znP1tk_rkCnyagH9FK205hHeE9k5mhUzOg/edit#heading=h.9dcmkavg608r + */ + peerConnection.createAnswer(sdpObserver, new MediaConstraints()); + + } + + if (peerConnection.getRemoteDescription() != null) { + drainIceCandidates(); + } + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/ProximitySensor.java b/app/src/main/java/com/nextcloud/talk/webrtc/ProximitySensor.java new file mode 100644 index 0000000..925f459 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/webrtc/ProximitySensor.java @@ -0,0 +1,155 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +import org.webrtc.ThreadUtils; + +/** + * ProximitySensor manages functions related to the proximity sensor in + * the app. + * On most device, the proximity sensor is implemented as a boolean-sensor. + * It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX + * value i.e. the LUX value of the light sensor is compared with a threshold. + * A LUX-value more than the threshold means the proximity sensor returns "FAR". + * Anything less than the threshold value and the sensor returns "NEAR". + */ +public class ProximitySensor implements SensorEventListener { + private static final String TAG = "ProximitySensor"; + + // This class should be created, started and stopped on one thread + // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is + // the case. Only active when |DEBUG| is set to true. + private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); + + private final Runnable onSensorStateListener; + private final SensorManager sensorManager; + private Sensor proximitySensor = null; + private boolean lastStateReportIsNear = false; + + private ProximitySensor(Context context, Runnable sensorStateListener) { + onSensorStateListener = sensorStateListener; + sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)); + } + + /** + * Construction + */ + static ProximitySensor create(Context context, Runnable sensorStateListener) { + return new ProximitySensor(context, sensorStateListener); + } + + /** + * Activate the proximity sensor. Also do initialization if called for the + * first time. + */ + public boolean start() { + threadChecker.checkIsOnValidThread(); + if (!initDefaultSensor()) { + // Proximity sensor is not supported on this device. + return false; + } + sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); + return true; + } + + /** + * Deactivate the proximity sensor. + */ + void stop() { + threadChecker.checkIsOnValidThread(); + if (proximitySensor == null) { + return; + } + sensorManager.unregisterListener(this, proximitySensor); + } + + /** + * Getter for last reported state. Set to true if "near" is reported. + */ + boolean sensorReportsNearState() { + threadChecker.checkIsOnValidThread(); + return lastStateReportIsNear; + } + + @Override + public final void onAccuracyChanged(Sensor sensor, int accuracy) { + threadChecker.checkIsOnValidThread(); + if (sensor.getType() == Sensor.TYPE_PROXIMITY && + accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) { + Log.e(TAG, "The values returned by this sensor cannot be trusted"); + } + } + + @Override + public final void onSensorChanged(SensorEvent event) { + threadChecker.checkIsOnValidThread(); + if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) { + // As a best practice; do as little as possible within this method and + // avoid blocking. + float distanceInCentimeters = event.values[0]; + if (distanceInCentimeters < proximitySensor.getMaximumRange()) { + Log.d(TAG, "Proximity sensor => NEAR state"); + lastStateReportIsNear = true; + } else { + Log.d(TAG, "Proximity sensor => FAR state"); + lastStateReportIsNear = false; + } + + // Report about new state to listening client. Client can then call + // sensorReportsNearState() to query the current state (NEAR or FAR). + if (onSensorStateListener != null) { + onSensorStateListener.run(); + } + } + } + + /** + * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) + * does not support this type of sensor and false will be returned in such + * cases. + */ + private boolean initDefaultSensor() { + if (proximitySensor != null) { + return true; + } + proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); + if (proximitySensor == null) { + return false; + } + logProximitySensorInfo(); + return true; + } + + /** + * Helper method for logging information about the proximity sensor. + */ + private void logProximitySensorInfo() { + if (proximitySensor == null) { + return; + } + StringBuilder info = new StringBuilder("Proximity sensor: "); + info.append("name=").append(proximitySensor.getName()) + .append(", vendor: ").append(proximitySensor.getVendor()) + .append(", power: ").append(proximitySensor.getPower()) + .append(", resolution: ").append(proximitySensor.getResolution()) + .append(", max range: ").append(proximitySensor.getMaximumRange()) + .append(", min delay: ").append(proximitySensor.getMinDelay()); + info.append(", type: ").append(proximitySensor.getStringType()); + info.append(", max delay: ").append(proximitySensor.getMaxDelay()) + .append(", reporting mode: ").append(proximitySensor.getReportingMode()) + .append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor()); + + Log.d(TAG, info.toString()); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebRTCUtils.java b/app/src/main/java/com/nextcloud/talk/webrtc/WebRTCUtils.java new file mode 100644 index 0000000..2fe3bf9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebRTCUtils.java @@ -0,0 +1,134 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-FileCopyrightText: 2016 The WebRTC Project Authors + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Original code: + * + * Copyright 2016 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +package com.nextcloud.talk.webrtc; + +import android.os.Build; +import android.util.Log; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class WebRTCUtils { + private static final String TAG = "WebRTCUtils"; + + private static final Set HARDWARE_ACCELERATION_DEVICE_EXCLUDE_SET = new HashSet() {{ + add("GT-I9100"); // Samsung Galaxy S2 + add("GT-N8013"); // Samsung Galaxy Note 10.1 + add("SM-G930F"); // Samsung Galaxy S7 + add("AGS-W09"); // Huawei MediaPad T3 10 + add("MIX 2"); // Xiaomi Mi Mix 2 + add("HUAWEI VNS-L31"); // Huawei P9 Lite + add("ALE-L21"); // Huawei P8 Lite + add("Z380M"); // Asus ZenPad 8.0 + add("XT1097"); // Motorola Moto X (2nd Gen) + }}; + + private static final Set HARDWARE_ACCELERATION_VENDOR_EXCLUDE_SET = new HashSet() {{ + add("samsung"); + }}; + + + public static boolean shouldEnableVideoHardwareAcceleration() { + return (!HARDWARE_ACCELERATION_VENDOR_EXCLUDE_SET.contains(Build.MANUFACTURER.toLowerCase(Locale.ROOT)) + && !HARDWARE_ACCELERATION_DEVICE_EXCLUDE_SET.contains(Build.MODEL.toUpperCase(Locale.ROOT))); + } + + public static String preferCodec(String sdpDescription, String codec, boolean isAudio) { + final String[] lines = sdpDescription.split("\r\n"); + final int mLineIndex = findMediaDescriptionLine(isAudio, lines); + if (mLineIndex == -1) { + Log.w(TAG, "No mediaDescription line, so can't prefer " + codec); + return sdpDescription; + } + // A list with all the payload types with name |codec|. The payload types are integers in the + // range 96-127, but they are stored as strings here. + final List codecPayloadTypes = new ArrayList<>(); + // a=rtpmap: / [/] + final Pattern codecPattern = Pattern.compile("^a=rtpmap:(\\d+) " + codec + "(/\\d+)+[\r]?$"); + for (int i = 0; i < lines.length; ++i) { + Matcher codecMatcher = codecPattern.matcher(lines[i]); + if (codecMatcher.matches()) { + codecPayloadTypes.add(codecMatcher.group(1)); + } + } + if (codecPayloadTypes.isEmpty()) { + Log.w(TAG, "No payload types with name " + codec); + return sdpDescription; + } + + final String newMLine = movePayloadTypesToFront(codecPayloadTypes, lines[mLineIndex]); + if (newMLine == null) { + return sdpDescription; + } + Log.d(TAG, "Change media description from: " + lines[mLineIndex] + " to " + newMLine); + lines[mLineIndex] = newMLine; + return joinString(Arrays.asList(lines), "\r\n", true); + } + + /** + * Returns the line number containing "m=audio|video", or -1 if no such line exists. + */ + private static int findMediaDescriptionLine(boolean isAudio, String[] sdpLines) { + final String mediaDescription = isAudio ? "m=audio " : "m=video "; + for (int i = 0; i < sdpLines.length; ++i) { + if (sdpLines[i].startsWith(mediaDescription)) { + return i; + } + } + return -1; + } + + private static String movePayloadTypesToFront(List preferredPayloadTypes, String mLine) { + // The format of the media description line should be: m= ... + final List origLineParts = Arrays.asList(mLine.split(" ")); + if (origLineParts.size() <= 3) { + Log.e(TAG, "Wrong SDP media description format: " + mLine); + return null; + } + final List header = origLineParts.subList(0, 3); + final List unpreferredPayloadTypes = + new ArrayList<>(origLineParts.subList(3, origLineParts.size())); + unpreferredPayloadTypes.removeAll(preferredPayloadTypes); + // Reconstruct the line with |preferredPayloadTypes| moved to the beginning of the payload + // types. + final List newLineParts = new ArrayList<>(); + newLineParts.addAll(header); + newLineParts.addAll(preferredPayloadTypes); + newLineParts.addAll(unpreferredPayloadTypes); + return joinString(newLineParts, " ", false /* delimiterAtEnd */); + } + + private static String joinString( + Iterable s, + String delimiter, + boolean delimiterAtEnd) { + Iterator iter = s.iterator(); + if (!iter.hasNext()) { + return ""; + } + StringBuilder buffer = new StringBuilder(iter.next()); + while (iter.hasNext()) { + buffer.append(delimiter).append(iter.next()); + } + if (delimiterAtEnd) { + buffer.append(delimiter); + } + return buffer.toString(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java new file mode 100644 index 0000000..4ea8f3b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java @@ -0,0 +1,581 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-FileCopyrightText: 2014 The WebRTC Project Authors + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Original code: + * + * Copyright 2014 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +package com.nextcloud.talk.webrtc; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.util.Log; + +import com.nextcloud.talk.events.ProximitySensorEvent; +import com.nextcloud.talk.utils.ContextExtensionsKt; +import com.nextcloud.talk.utils.ReceiverFlag; +import com.nextcloud.talk.utils.power.PowerManagerUtils; + +import org.greenrobot.eventbus.EventBus; +import org.webrtc.ThreadUtils; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class WebRtcAudioManager { + private static final String TAG = WebRtcAudioManager.class.getSimpleName(); + private final Context context; + private final WebRtcBluetoothManager bluetoothManager; + private final boolean useProximitySensor; + private final AudioManager audioManager; + private AudioManagerListener audioManagerListener; + private AudioManagerState amState; + private int savedAudioMode = AudioManager.MODE_INVALID; + private boolean savedIsSpeakerPhoneOn = false; + private boolean savedIsMicrophoneMute = false; + private boolean hasWiredHeadset = false; + + private AudioDevice userSelectedAudioDevice; + private AudioDevice currentAudioDevice; + private AudioDevice defaultAudioDevice; + + private ProximitySensor proximitySensor = null; + + private Set audioDevices = new HashSet<>(); + + private Set internalAudioDevices = new HashSet<>(); + + private final BroadcastReceiver wiredHeadsetReceiver; + private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; + + private final PowerManagerUtils powerManagerUtils; + + private WebRtcAudioManager(Context context, boolean useProximitySensor) { + Log.d(TAG, "ctor"); + ThreadUtils.checkIsOnMainThread(); + this.context = context; + audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); + bluetoothManager = WebRtcBluetoothManager.create(context, this); + wiredHeadsetReceiver = new WiredHeadsetReceiver(); + amState = AudioManagerState.UNINITIALIZED; + + powerManagerUtils = new PowerManagerUtils(); + powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITH_PROXIMITY_SENSOR_LOCK); + + this.useProximitySensor = useProximitySensor; + updateAudioDeviceState(); + + // Create and initialize the proximity sensor. + // Tablet devices (e.g. Nexus 7) does not support proximity sensors. + // Note that, the sensor will not be active until start() has been called. + proximitySensor = ProximitySensor.create(context, new Runnable() { + // This method will be called each time a state change is detected. + // Example: user holds his hand over the device (closer than ~5 cm), + // or removes his hand from the device. + public void run() { + onProximitySensorChangedState(); + } + }); + } + + /** + * Construction. + */ + public static WebRtcAudioManager create(Context context, boolean useProximitySensor) { + return new WebRtcAudioManager(context, useProximitySensor); + } + + public void startBluetoothManager() { + // Initialize and start Bluetooth if a BT device is available or initiate + // detection of new (enabled) BT devices. + bluetoothManager.start(); + } + + /** + * This method is called when the proximity sensor reports a state change, e.g. from "NEAR to FAR" or from "FAR to + * NEAR". + */ + private void onProximitySensorChangedState() { + if (!useProximitySensor) { + return; + } + + if (userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE + && audioDevices.contains(AudioDevice.EARPIECE) + && audioDevices.contains(AudioDevice.SPEAKER_PHONE)) { + + if (proximitySensor.sensorReportsNearState()) { + setAudioDeviceInternal(AudioDevice.EARPIECE); + Log.d(TAG, "switched to EARPIECE because userSelectedAudioDevice was SPEAKER_PHONE and proximity=near"); + + EventBus.getDefault().post(new ProximitySensorEvent(ProximitySensorEvent.ProximitySensorEventType.SENSOR_NEAR)); + + } else { + setAudioDeviceInternal(WebRtcAudioManager.AudioDevice.SPEAKER_PHONE); + Log.d(TAG, "switched to SPEAKER_PHONE because userSelectedAudioDevice was SPEAKER_PHONE and proximity=far"); + + EventBus.getDefault().post(new ProximitySensorEvent(ProximitySensorEvent.ProximitySensorEventType.SENSOR_FAR)); + } + } + } + + @SuppressLint("WrongConstant") + public void start(AudioManagerListener audioManagerListener) { + Log.d(TAG, "start"); + ThreadUtils.checkIsOnMainThread(); + if (amState == AudioManagerState.RUNNING) { + Log.e(TAG, "AudioManager is already active"); + return; + } + // TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED. + + Log.d(TAG, "AudioManager starts..."); + this.audioManagerListener = audioManagerListener; + amState = AudioManagerState.RUNNING; + + // Store current audio state so we can restore it when stop() is called. + savedAudioMode = audioManager.getMode(); + savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); + savedIsMicrophoneMute = audioManager.isMicrophoneMute(); + hasWiredHeadset = hasWiredHeadset(); + + // Create an AudioManager.OnAudioFocusChangeListener instance. + audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { + // Called on the listener to notify if the audio focus for this listener has been changed. + // The |focusChange| value indicates whether the focus was gained, whether the focus was lost, + // and whether that loss is transient, or whether the new focus holder will hold it for an + // unknown amount of time. + // TODO(henrika): possibly extend support of handling audio-focus changes. Only contains + // logging for now. + @Override + public void onAudioFocusChange(int focusChange) { + String typeOfChange = "AUDIOFOCUS_NOT_DEFINED"; + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: + typeOfChange = "AUDIOFOCUS_GAIN"; + break; + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT: + typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT"; + break; + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: + typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE"; + break; + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: + typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK"; + break; + case AudioManager.AUDIOFOCUS_LOSS: + typeOfChange = "AUDIOFOCUS_LOSS"; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT"; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK"; + break; + default: + typeOfChange = "AUDIOFOCUS_INVALID"; + break; + } + Log.d(TAG, "onAudioFocusChange: " + typeOfChange); + } + }; + + // Request audio playout focus (without ducking) and install listener for changes in focus. + int result = audioManager.requestAudioFocus(audioFocusChangeListener, + AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + Log.d(TAG, "Audio focus request granted for VOICE_CALL streams"); + } else { + Log.e(TAG, "Audio focus request failed"); + } + + // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is + // required to be in this mode when playout and/or recording starts for + // best possible VoIP performance. + audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); + + // Always disable microphone mute during a WebRTC call. + setMicrophoneMute(false); + + // Set initial device states. + userSelectedAudioDevice = AudioDevice.NONE; + currentAudioDevice = AudioDevice.NONE; + defaultAudioDevice = AudioDevice.NONE; + audioDevices.clear(); + internalAudioDevices.clear(); + + startBluetoothManager(); + + // Do initial selection of audio device. This setting can later be changed + // either by adding/removing a BT or wired headset or by covering/uncovering + // the proximity sensor. + updateAudioDeviceState(); + + proximitySensor.start(); + // Register receiver for broadcast intents related to adding/removing a + // wired headset. + registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); + Log.d(TAG, "AudioManager started"); + } + + @SuppressLint("WrongConstant") + public void stop() { + Log.d(TAG, "stop"); + ThreadUtils.checkIsOnMainThread(); + if (amState != AudioManagerState.RUNNING) { + Log.e(TAG, "Trying to stop AudioManager in incorrect state: " + amState); + return; + } + amState = AudioManagerState.UNINITIALIZED; + + unregisterReceiver(wiredHeadsetReceiver); + + if(bluetoothManager.started()) { + bluetoothManager.stop(); + } + + // Restore previously stored audio states. + setSpeakerphoneOn(savedIsSpeakerPhoneOn); + setMicrophoneMute(savedIsMicrophoneMute); + audioManager.setMode(savedAudioMode); + + // Abandon audio focus. Gives the previous focus owner, if any, focus. + audioManager.abandonAudioFocus(audioFocusChangeListener); + audioFocusChangeListener = null; + Log.d(TAG, "Abandoned audio focus for VOICE_CALL streams"); + + if (proximitySensor != null) { + proximitySensor.stop(); + proximitySensor = null; + } + + powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.IDLE); + + audioManagerListener = null; + Log.d(TAG, "AudioManager stopped"); + } + + ; + + /** + * Changes selection of the currently active audio device. + */ + private void setAudioDeviceInternal(AudioDevice audioDevice) { + Log.d(TAG, "setAudioDeviceInternal(device=" + audioDevice + ")"); + + if (audioDevices.contains(audioDevice)) { + switch (audioDevice) { + case SPEAKER_PHONE: + setSpeakerphoneOn(true); + break; + case EARPIECE: + case WIRED_HEADSET: + case BLUETOOTH: + setSpeakerphoneOn(false); + break; + default: + Log.e(TAG, "Invalid audio device selection"); + break; + } + currentAudioDevice = audioDevice; + } + } + + /** + * Sets the default audio device to use if selection algo has no other option + */ + public void setDefaultAudioDevice(AudioDevice device) { + ThreadUtils.checkIsOnMainThread(); + if (!audioDevices.contains(device)) { + Log.e(TAG, "Can not select default " + device + " from available " + audioDevices); + } + defaultAudioDevice = device; + updateAudioDeviceState(); + } + + /** + * Changes selection of the currently active audio device. + */ + public void selectAudioDevice(AudioDevice device) { + ThreadUtils.checkIsOnMainThread(); + if (!audioDevices.contains(device)) { + Log.e(TAG, "Can not select " + device + " from available " + audioDevices); + } + userSelectedAudioDevice = device; + updateAudioDeviceState(); + } + + /** + * Returns current set of available/selectable audio devices. + */ + public Set getAudioDevices() { + ThreadUtils.checkIsOnMainThread(); + return Collections.unmodifiableSet(new HashSet(audioDevices)); + } + + /** + * Returns the currently selected audio device. + */ + public AudioDevice getCurrentAudioDevice() { + ThreadUtils.checkIsOnMainThread(); + return currentAudioDevice; + } + + /** + * Helper method for receiver registration. + */ + private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + ContextExtensionsKt.registerBroadcastReceiver(context, receiver, filter, ReceiverFlag.NotExported); + } + + /** + * Helper method for unregistration of an existing receiver. + */ + private void unregisterReceiver(BroadcastReceiver receiver) { + context.unregisterReceiver(receiver); + } + + /** + * Sets the speaker phone mode. + */ + private void setSpeakerphoneOn(boolean on) { + boolean wasOn = audioManager.isSpeakerphoneOn(); + if (wasOn == on) { + return; + } + audioManager.setSpeakerphoneOn(on); + } + + /** + * Sets the microphone mute state. + */ + private void setMicrophoneMute(boolean on) { + boolean wasMuted = audioManager.isMicrophoneMute(); + if (wasMuted == on) { + return; + } + audioManager.setMicrophoneMute(on); + } + + /** + * Gets the current earpiece state. + */ + private boolean hasEarpiece() { + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); + } + + /** + * Checks whether a wired headset is connected or not. This is not a valid indication that audio playback is + * actually over the wired headset as audio routing depends on other conditions. We only use it as an early + * indicator (during initialization) of an attached wired headset. + */ + @Deprecated + private boolean hasWiredHeadset() { + @SuppressLint("WrongConstant") final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL); + for (AudioDeviceInfo device : devices) { + final int type = device.getType(); + if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) { + Log.d(TAG, "hasWiredHeadset: found wired headset"); + return true; + } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) { + Log.d(TAG, "hasWiredHeadset: found USB audio device"); + return true; + } + } + return false; + } + + public final void updateAudioDeviceState() { + ThreadUtils.checkIsOnMainThread(); + Log.d(TAG, "--- updateAudioDeviceState: " + + "wired headset=" + hasWiredHeadset + ", " + + "BT state=" + bluetoothManager.getState()); + Log.d(TAG, "Device status: " + + "internally available=" + internalAudioDevices + ", " + + "externally available=" + audioDevices + ", " + + "default=" + defaultAudioDevice + ", " + + "current=" + currentAudioDevice + ", " + + "user selected=" + userSelectedAudioDevice); + + if (bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE + || bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_UNAVAILABLE + || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_DISCONNECTING) { + bluetoothManager.updateDevice(); + } + + Set newInternalAudioDevices = new HashSet<>(); + + if (bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED + || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTING + || bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE) { + newInternalAudioDevices.add(AudioDevice.BLUETOOTH); + } + + if (bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED) { + newInternalAudioDevices.add(AudioDevice.BLUETOOTH_SCO); + } + + if (hasWiredHeadset) { + // If a wired headset is connected, then it is the only possible option. + newInternalAudioDevices.add(AudioDevice.WIRED_HEADSET); + } else { + newInternalAudioDevices.add(AudioDevice.SPEAKER_PHONE); + if (hasEarpiece()) { + newInternalAudioDevices.add(AudioDevice.EARPIECE); + } + } + + // Correct user selected audio devices if needed. + if (userSelectedAudioDevice == AudioDevice.BLUETOOTH + && bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_UNAVAILABLE) { + userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; + } + if (userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE && hasWiredHeadset) { + userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; + } + if (userSelectedAudioDevice == AudioDevice.WIRED_HEADSET && !hasWiredHeadset) { + userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; + } + + + // Need to start Bluetooth if it is available and user either selected it explicitly or + // user did not select any output device. + boolean needBluetoothScoStart = + bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE + && (userSelectedAudioDevice == AudioDevice.NONE + || userSelectedAudioDevice == AudioDevice.BLUETOOTH); + + // Need to stop Bluetooth audio if user selected different device and + // Bluetooth SCO connection is established or in the process. + boolean needBluetoothScoStop = + (bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED + || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTING) + && (userSelectedAudioDevice != AudioDevice.NONE + && userSelectedAudioDevice != AudioDevice.BLUETOOTH); + + if (bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE + || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTING + || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED) { + Log.d(TAG, "Need BT audio: start=" + needBluetoothScoStart + ", " + + "stop=" + needBluetoothScoStop + ", " + + "BT state=" + bluetoothManager.getState()); + } + + // Start or stop Bluetooth SCO connection given states set earlier. + if (needBluetoothScoStop) { + bluetoothManager.stopScoAudio(); + bluetoothManager.updateDevice(); + } else if (needBluetoothScoStart && !bluetoothManager.startScoAudio()) { + // Remove BLUETOOTH and BLUETOOTH_SCO from list of available devices since SCO start has + // reported no longer available or too many failed attempts. + newInternalAudioDevices.remove(AudioDevice.BLUETOOTH); + newInternalAudioDevices.remove(AudioDevice.BLUETOOTH_SCO); + } + + boolean audioDeviceSetUpdated = !internalAudioDevices.equals(newInternalAudioDevices); + internalAudioDevices = newInternalAudioDevices; + // BLUETOOTH_SCO isn't allowed to be in the externally accessible list of devices + audioDevices = new HashSet<>(internalAudioDevices); + audioDevices.remove(AudioDevice.BLUETOOTH_SCO); + + + // Update selected audio device. + AudioDevice newCurrentAudioDevice; + + if ((bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED) + && newInternalAudioDevices.contains(AudioDevice.BLUETOOTH_SCO)) + { + // If Bluetooth SCO is connected and available to use, then it has been selected by user or + // auto-selected and it should be used as output audio device. + newCurrentAudioDevice = AudioDevice.BLUETOOTH; + } else if (hasWiredHeadset) { + // If a wired headset is connected, but Bluetooth SCO is not, then wired headset is used as + // audio device. + newCurrentAudioDevice = AudioDevice.WIRED_HEADSET; + } else { + // No wired headset and no Bluetooth SCO, hence the audio-device list can contain speaker + // phone (on a tablet), or speaker phone and earpiece (on mobile phone). + // |userSelectedAudioDevice| may contain either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE + // depending on the user's selection. |defaultAudioDevice|, which is set in code depending on + // call is audio only or video, to be used if user hasn't made an explicit selection + if ((userSelectedAudioDevice == AudioDevice.NONE) && (defaultAudioDevice != AudioDevice.NONE)) + newCurrentAudioDevice = defaultAudioDevice; + else + newCurrentAudioDevice = userSelectedAudioDevice; + } + // Switch to new device but only if there has been any changes. + if (newCurrentAudioDevice != currentAudioDevice || audioDeviceSetUpdated) { + // Do the required device switch. + setAudioDeviceInternal(newCurrentAudioDevice); + Log.d(TAG, "New device status: " + + "internally available=" + internalAudioDevices + ", " + + "externally available=" + audioDevices + ", " + + "current(new)=" + newCurrentAudioDevice); + if (audioManagerListener != null) { + // Notify a listening client that audio device has been changed. + audioManagerListener.onAudioDeviceChanged(currentAudioDevice, audioDevices); + } + } + Log.d(TAG, "--- updateAudioDeviceState done"); + } + + /** + * AudioDevice is the names of possible audio devices that we currently support. + */ + public enum AudioDevice { + SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE, + BLUETOOTH_SCO // BLUETOOTH_SCO is only valid internal to this class + } + + /** + * AudioManager state. + */ + public enum AudioManagerState { + UNINITIALIZED, + PREINITIALIZED, + RUNNING, + } + + /** + * Selected audio device change event. + */ + public static interface AudioManagerListener { + // Callback fired once audio device is changed or list of available audio devices changed. + void onAudioDeviceChanged( + AudioDevice selectedAudioDevice, Set availableAudioDevices); + } + + /* Receiver which handles changes in wired headset availability. */ + private class WiredHeadsetReceiver extends BroadcastReceiver { + private static final int STATE_UNPLUGGED = 0; + private static final int STATE_PLUGGED = 1; + private static final int HAS_NO_MIC = 0; + + @Override + public void onReceive(Context context, Intent intent) { + int state = intent.getIntExtra("state", STATE_UNPLUGGED); + // int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); + // String name = intent.getStringExtra("name"); + hasWiredHeadset = (state == STATE_PLUGGED); + updateAudioDeviceState(); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcBluetoothManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcBluetoothManager.java new file mode 100644 index 0000000..7835e6f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcBluetoothManager.java @@ -0,0 +1,582 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-FileCopyrightText: 2016 The WebRTC Project Authors + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Original code: + * + * Copyright 2016 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +package com.nextcloud.talk.webrtc; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothProfile; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.media.AudioManager; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.util.Log; + +import com.nextcloud.talk.utils.ContextExtensionsKt; +import com.nextcloud.talk.utils.ReceiverFlag; + +import org.webrtc.ThreadUtils; + +import java.util.List; +import java.util.Set; + +import androidx.core.app.ActivityCompat; + +public class WebRtcBluetoothManager { + private static final String TAG = WebRtcBluetoothManager.class.getSimpleName(); + + // Timeout interval for starting or stopping audio to a Bluetooth SCO device. + private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; + // Maximum number of SCO connection attempts. + private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; + private final Context apprtcContext; + private final WebRtcAudioManager webRtcAudioManager; + private final AudioManager audioManager; + private final Handler handler; + private final BluetoothProfile.ServiceListener bluetoothServiceListener; + private final BroadcastReceiver bluetoothHeadsetReceiver; + int scoConnectionAttempts; + private State bluetoothState; + private BluetoothAdapter bluetoothAdapter; + private BluetoothHeadset bluetoothHeadset; + private BluetoothDevice bluetoothDevice; + // Runs when the Bluetooth timeout expires. We use that timeout after calling + // startScoAudio() or stopScoAudio() because we're not guaranteed to get a + // callback after those calls. + private final Runnable bluetoothTimeoutRunnable = this::bluetoothTimeout; + private boolean started = false; + + protected WebRtcBluetoothManager(Context context, WebRtcAudioManager audioManager) { + Log.d(TAG, "ctor"); + ThreadUtils.checkIsOnMainThread(); + apprtcContext = context; + webRtcAudioManager = audioManager; + this.audioManager = getAudioManager(context); + bluetoothState = State.UNINITIALIZED; + bluetoothServiceListener = new BluetoothServiceListener(); + bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver(); + handler = new Handler(Looper.getMainLooper()); + } + + /** + * Construction. + */ + static WebRtcBluetoothManager create(Context context, WebRtcAudioManager audioManager) { + return new WebRtcBluetoothManager(context, audioManager); + } + + /** + * Returns the internal state. + */ + public State getState() { + ThreadUtils.checkIsOnMainThread(); + return bluetoothState; + } + + /** + * Activates components required to detect Bluetooth devices and to enable + * BT SCO (audio is routed via BT SCO) for the headset profile. The end + * state will be HEADSET_UNAVAILABLE but a state machine has started which + * will start a state change sequence where the final outcome depends on + * if/when the BT headset is enabled. + * Example of state change sequence when start() is called while BT device + * is connected and enabled: + * UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE --> + * SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO. + * Note that the AudioManager is also involved in driving this state + * change. + */ + @SuppressLint("MissingPermission") + public void start() { + ThreadUtils.checkIsOnMainThread(); + Log.d(TAG, "start"); + if(hasNoBluetoothPermission()){ + return; + } + if (bluetoothState != State.UNINITIALIZED) { + Log.w(TAG, "Invalid BT state"); + return; + } + bluetoothHeadset = null; + bluetoothDevice = null; + scoConnectionAttempts = 0; + // Get a handle to the default local Bluetooth adapter. + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (bluetoothAdapter == null) { + Log.w(TAG, "Device does not support Bluetooth"); + return; + } + // Ensure that the device supports use of BT SCO audio for off call use cases. + if (!audioManager.isBluetoothScoAvailableOffCall()) { + Log.e(TAG, "Bluetooth SCO audio is not available off call"); + return; + } + logBluetoothAdapterInfo(bluetoothAdapter); + // Establish a connection to the HEADSET profile (includes both Bluetooth Headset and + // Hands-Free) proxy object and install a listener. + if (!getBluetoothProfileProxy( + apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) { + Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed"); + return; + } + // Register receivers for BluetoothHeadset change notifications. + IntentFilter bluetoothHeadsetFilter = new IntentFilter(); + // Register receiver for change in connection state of the Headset profile. + bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); + // Register receiver for change in audio connection state of the Headset profile. + bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); + registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); + Log.d(TAG, "HEADSET profile state: " + + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET))); + Log.d(TAG, "Bluetooth proxy for headset profile has started"); + bluetoothState = State.HEADSET_UNAVAILABLE; + started = true; + Log.d(TAG, "start done: BT state=" + bluetoothState); + } + + /** + * Stops and closes all components related to Bluetooth audio. + */ + public void stop() { + ThreadUtils.checkIsOnMainThread(); + Log.d(TAG, "stop: BT state=" + bluetoothState); + if (bluetoothAdapter == null) { + return; + } + // Stop BT SCO connection with remote device if needed. + stopScoAudio(); + // Close down remaining BT resources. + if (bluetoothState == State.UNINITIALIZED) { + return; + } + unregisterReceiver(bluetoothHeadsetReceiver); + cancelTimer(); + if (bluetoothHeadset != null) { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset); + bluetoothHeadset = null; + } + bluetoothAdapter = null; + bluetoothDevice = null; + bluetoothState = State.UNINITIALIZED; + Log.d(TAG, "stop done: BT state=" + bluetoothState); + } + + /** + * Starts Bluetooth SCO connection with remote device. + * Note that the phone application always has the priority on the usage of the SCO connection + * for telephony. If this method is called while the phone is in call it will be ignored. + * Similarly, if a call is received or sent while an application is using the SCO connection, + * the connection will be lost for the application and NOT returned automatically when the call + * ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a + * virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO + * audio connection is established. + * TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and + * higher. It might be required to initiates a virtual voice call since many devices do not + * accept SCO audio without a "call". + */ + public boolean startScoAudio() { + ThreadUtils.checkIsOnMainThread(); + Log.d(TAG, "startSco: BT state=" + bluetoothState + ", " + + "attempts: " + scoConnectionAttempts + ", " + + "SCO is on: " + isScoOn()); + if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) { + Log.e(TAG, "BT SCO connection fails - no more attempts"); + return false; + } + if (bluetoothState != State.HEADSET_AVAILABLE) { + Log.e(TAG, "BT SCO connection fails - no headset available"); + return false; + } + // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED. + Log.d(TAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED..."); + // The SCO connection establishment can take several seconds, hence we cannot rely on the + // connection to be available when the method returns but instead register to receive the + // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED. + bluetoothState = State.SCO_CONNECTING; + audioManager.startBluetoothSco(); + audioManager.setBluetoothScoOn(true); + scoConnectionAttempts++; + startTimer(); + Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", " + + "SCO is on: " + isScoOn()); + return true; + } + + /** + * Stops Bluetooth SCO connection with remote device. + */ + public void stopScoAudio() { + ThreadUtils.checkIsOnMainThread(); + Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + + "SCO is on: " + isScoOn()); + if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) { + return; + } + cancelTimer(); + audioManager.stopBluetoothSco(); + audioManager.setBluetoothScoOn(false); + bluetoothState = State.SCO_DISCONNECTING; + Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", " + + "SCO is on: " + isScoOn()); + } + + /** + * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset + * Service via IPC) to update the list of connected devices for the HEADSET + * profile. The internal state will change to HEADSET_UNAVAILABLE or to + * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected + * device if available. + */ + @SuppressLint("MissingPermission") + public void updateDevice() { + boolean hasNoBluetoothPermissions = hasNoBluetoothPermission(); + if (hasNoBluetoothPermissions || + bluetoothState == State.UNINITIALIZED || + bluetoothHeadset == null) { + return; + } + Log.d(TAG, "updateDevice"); + // Get connected devices for the headset profile. Returns the set of + // devices which are in state STATE_CONNECTED. The BluetoothDevice class + // is just a thin wrapper for a Bluetooth hardware address. + List devices = bluetoothHeadset.getConnectedDevices(); + if (devices.isEmpty()) { + bluetoothDevice = null; + bluetoothState = State.HEADSET_UNAVAILABLE; + Log.d(TAG, "No connected bluetooth headset"); + } else { + // Always use first device in list. Android only supports one device. + bluetoothDevice = devices.get(0); + bluetoothState = State.HEADSET_AVAILABLE; + Log.d(TAG, "Connected bluetooth headset: " + + "name=" + bluetoothDevice.getName() + ", " + + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice)); + } + Log.d(TAG, "updateDevice done: BT state=" + bluetoothState); + } + + /** + * Stubs for test mocks. + */ + protected final AudioManager getAudioManager(Context context) { + return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + } + + protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + ContextExtensionsKt.registerBroadcastReceiver( + apprtcContext, + receiver, + filter, + ReceiverFlag.Exported); + } + + protected void unregisterReceiver(BroadcastReceiver receiver) { + apprtcContext.unregisterReceiver(receiver); + } + + protected boolean getBluetoothProfileProxy( + Context context, BluetoothProfile.ServiceListener listener, int profile) { + return bluetoothAdapter.getProfileProxy(context, listener, profile); + } + + private boolean hasNoBluetoothPermission() { + String permission; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permission = Manifest.permission.BLUETOOTH_CONNECT; + } else { + permission = Manifest.permission.BLUETOOTH; + } + + boolean hasPermission = + ActivityCompat.checkSelfPermission(apprtcContext, permission) == PackageManager.PERMISSION_GRANTED; + if(!hasPermission) { + Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks \"" + permission + "\" permission"); + } + return !hasPermission; + } + + /** + * Logs the state of the local Bluetooth adapter. + */ + @SuppressLint({"HardwareIds", "MissingPermission"}) + private void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { + Log.d(TAG, "BluetoothAdapter: " + + "enabled=" + localAdapter.isEnabled() + ", " + + "state=" + stateToString(localAdapter.getState()) + ", " + + "name=" + localAdapter.getName()); + // Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter. + Set pairedDevices = localAdapter.getBondedDevices(); + if (!pairedDevices.isEmpty()) { + Log.d(TAG, "paired devices:"); + for (BluetoothDevice device : pairedDevices) { + Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress()); + } + } + } + + /** + * Ensures that the audio manager updates its list of available audio devices. + */ + private void updateAudioDeviceState() { + ThreadUtils.checkIsOnMainThread(); + Log.d(TAG, "updateAudioDeviceState"); + webRtcAudioManager.updateAudioDeviceState(); + } + + /** + * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. + */ + private void startTimer() { + ThreadUtils.checkIsOnMainThread(); + Log.d(TAG, "startTimer"); + handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS); + } + + /** + * Cancels any outstanding timer tasks. + */ + private void cancelTimer() { + ThreadUtils.checkIsOnMainThread(); + Log.d(TAG, "cancelTimer"); + handler.removeCallbacks(bluetoothTimeoutRunnable); + } + + /** + * Called when start of the BT SCO channel takes too long time. Usually + * happens when the BT device has been turned on during an ongoing call. + */ + @SuppressLint("MissingPermission") + private void bluetoothTimeout() { + ThreadUtils.checkIsOnMainThread(); + boolean hasNoBluetoothPermissions = hasNoBluetoothPermission(); + if (hasNoBluetoothPermissions || + bluetoothState == State.UNINITIALIZED || + bluetoothHeadset == null) { + return; + } + Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + + "attempts: " + scoConnectionAttempts + ", " + + "SCO is on: " + isScoOn()); + if (bluetoothState != State.SCO_CONNECTING) { + return; + } + // Bluetooth SCO should be connecting; check the latest result. + boolean scoConnected = false; + List devices = bluetoothHeadset.getConnectedDevices(); + if (devices.size() > 0) { + bluetoothDevice = devices.get(0); + if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) { + Log.d(TAG, "SCO connected with " + bluetoothDevice.getName()); + scoConnected = true; + } else { + Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName()); + } + } + if (scoConnected) { + // We thought BT had timed out, but it's actually on; updating state. + bluetoothState = State.SCO_CONNECTED; + scoConnectionAttempts = 0; + } else { + // Give up and "cancel" our request by calling stopBluetoothSco(). + Log.w(TAG, "BT failed to connect after timeout"); + stopScoAudio(); + } + updateAudioDeviceState(); + Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState); + } + + /** + * Checks whether audio uses Bluetooth SCO. + */ + private boolean isScoOn() { + return audioManager.isBluetoothScoOn(); + } + + /** + * Converts BluetoothAdapter states into local string representations. + */ + private String stateToString(int state) { + switch (state) { + case BluetoothAdapter.STATE_DISCONNECTED: + return "DISCONNECTED"; + case BluetoothAdapter.STATE_CONNECTED: + return "CONNECTED"; + case BluetoothAdapter.STATE_CONNECTING: + return "CONNECTING"; + case BluetoothAdapter.STATE_DISCONNECTING: + return "DISCONNECTING"; + case BluetoothAdapter.STATE_OFF: + return "OFF"; + case BluetoothAdapter.STATE_ON: + return "ON"; + case BluetoothAdapter.STATE_TURNING_OFF: + // Indicates the local Bluetooth adapter is turning off. Local clients should immediately + // attempt graceful disconnection of any remote links. + return "TURNING_OFF"; + case BluetoothAdapter.STATE_TURNING_ON: + // Indicates the local Bluetooth adapter is turning on. However local clients should wait + // for STATE_ON before attempting to use the adapter. + return "TURNING_ON"; + default: + return "INVALID"; + } + } + + public boolean started() { + return started; + } + + // Bluetooth connection state. + public enum State { + // Bluetooth is not available; no adapter or Bluetooth is off. + UNINITIALIZED, + // Bluetooth error happened when trying to start Bluetooth. + ERROR, + // Bluetooth proxy object for the Headset profile exists, but no connected headset devices, + // SCO is not started or disconnected. + HEADSET_UNAVAILABLE, + // Bluetooth proxy object for the Headset profile connected, connected Bluetooth headset + // present, but SCO is not started or disconnected. + HEADSET_AVAILABLE, + // Bluetooth audio SCO connection with remote device is closing. + SCO_DISCONNECTING, + // Bluetooth audio SCO connection with remote device is initiated. + SCO_CONNECTING, + // Bluetooth audio SCO connection with remote device is established. + SCO_CONNECTED + } + + /** + * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been + * connected to or disconnected from the service. + */ + private class BluetoothServiceListener implements BluetoothProfile.ServiceListener { + @Override + // Called to notify the client when the proxy object has been connected to the service. + // Once we have the profile proxy object, we can use it to monitor the state of the + // connection and perform other operations that are relevant to the headset profile. + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { + return; + } + Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); + // Android only supports one connected Bluetooth Headset at a time. + bluetoothHeadset = (BluetoothHeadset) proxy; + updateAudioDeviceState(); + Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState); + } + + /** + * Notifies the client when the proxy object has been disconnected from the service. + */ + @Override + public void onServiceDisconnected(int profile) { + if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { + return; + } + Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); + stopScoAudio(); + bluetoothHeadset = null; + bluetoothDevice = null; + bluetoothState = State.HEADSET_UNAVAILABLE; + updateAudioDeviceState(); + Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState); + } + } + + // Intent broadcast receiver which handles changes in Bluetooth device availability. + // Detects headset changes and Bluetooth SCO state changes. + private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (bluetoothState == State.UNINITIALIZED) { + return; + } + final String action = intent.getAction(); + // Change in connection state of the Headset profile. Note that the + // change does not tell us anything about whether we're streaming + // audio to BT over SCO. Typically received when user turns on a BT + // headset while audio is active using another audio device. + if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { + final int state = + intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); + Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + + "a=ACTION_CONNECTION_STATE_CHANGED, " + + "s=" + stateToString(state) + ", " + + "sb=" + isInitialStickyBroadcast() + ", " + + "BT state: " + bluetoothState); + if (state == BluetoothHeadset.STATE_CONNECTED) { + scoConnectionAttempts = 0; + updateAudioDeviceState(); + } else if (state == BluetoothHeadset.STATE_CONNECTING) { + Log.d(TAG, "+++ Bluetooth is connecting..."); + // No action needed. + } else if (state == BluetoothHeadset.STATE_DISCONNECTING) { + Log.d(TAG, "+++ Bluetooth is disconnecting..."); + // No action needed. + } else if (state == BluetoothHeadset.STATE_DISCONNECTED) { + // Bluetooth is probably powered off during the call. + stopScoAudio(); + updateAudioDeviceState(); + } + // Change in the audio (SCO) connection state of the Headset profile. + // Typically received after call to startScoAudio() has finalized. + } else if (BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED.equals(action)) { + final int state = intent.getIntExtra( + BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); + Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + + "a=ACTION_AUDIO_STATE_CHANGED, " + + "s=" + stateToString(state) + ", " + + "sb=" + isInitialStickyBroadcast() + ", " + + "BT state: " + bluetoothState); + if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { + cancelTimer(); + if (bluetoothState == State.SCO_CONNECTING) { + Log.d(TAG, "+++ Bluetooth audio SCO is now connected"); + bluetoothState = State.SCO_CONNECTED; + scoConnectionAttempts = 0; + updateAudioDeviceState(); + } else { + Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); + } + } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { + Log.d(TAG, "+++ Bluetooth audio SCO is now connecting..."); + } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { + Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected"); + if (isInitialStickyBroadcast()) { + Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); + return; + } + updateAudioDeviceState(); + } + } + Log.d(TAG, "onReceive done: BT state=" + bluetoothState); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java new file mode 100644 index 0000000..57452c0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java @@ -0,0 +1,171 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc; + +import android.annotation.SuppressLint; +import android.util.Log; + +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.data.user.model.User; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; +import com.nextcloud.talk.models.json.signaling.settings.FederationSettings; +import com.nextcloud.talk.models.json.websocket.ActorWebSocketMessage; +import com.nextcloud.talk.models.json.websocket.AuthParametersWebSocketMessage; +import com.nextcloud.talk.models.json.websocket.AuthWebSocketMessage; +import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage; +import com.nextcloud.talk.models.json.websocket.CallWebSocketMessage; +import com.nextcloud.talk.models.json.websocket.HelloOverallWebSocketMessage; +import com.nextcloud.talk.models.json.websocket.HelloWebSocketMessage; +import com.nextcloud.talk.models.json.websocket.RoomFederationWebSocketMessage; +import com.nextcloud.talk.models.json.websocket.RoomOverallWebSocketMessage; +import com.nextcloud.talk.models.json.websocket.RoomWebSocketMessage; +import com.nextcloud.talk.utils.ApiUtils; + +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; + +import autodagger.AutoInjector; +import okhttp3.OkHttpClient; + +@AutoInjector(NextcloudTalkApplication.class) +public class WebSocketConnectionHelper { + public static final String TAG = "WebSocketConnectionHelper"; + private static Map webSocketInstanceMap = new HashMap<>(); + + @Inject + OkHttpClient okHttpClient; + + + public WebSocketConnectionHelper() { + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + } + + @SuppressLint("LongLogTag") + public static synchronized WebSocketInstance getWebSocketInstanceForUser(User user) { + WebSocketInstance webSocketInstance = webSocketInstanceMap.get(user.getId()); + + if (webSocketInstance == null) { + Log.d(TAG, "No webSocketInstance found for user " + user.getDisplayName() + + " (userId:" + user.getId() + ")"); + } else { + Log.d(TAG, "Existing webSocketInstance found for user " + user.getDisplayName() + + " (userId:" + user.getId() + ")"); + } + + return webSocketInstance; + } + + public static synchronized WebSocketInstance getExternalSignalingInstanceForServer(String url, + User user, + String webSocketTicket, + boolean isGuest) { + String generatedURL = url.replace("https://", "wss://").replace("http://", "ws://"); + + if (generatedURL.endsWith("/")) { + generatedURL += "spreed"; + } else { + generatedURL += "/spreed"; + } + + long userId = isGuest ? -1 : user.getId(); + + WebSocketInstance webSocketInstance = getWebSocketInstanceForUser(user); + + if (userId != -1 && webSocketInstance != null && webSocketInstance.isConnected()) { + return webSocketInstance; + } + + if (userId == -1) { + deleteExternalSignalingInstanceForUserEntity(userId); + } + + webSocketInstance = new WebSocketInstance(user, generatedURL, webSocketTicket); + webSocketInstanceMap.put(user.getId(), webSocketInstance); + return webSocketInstance; + } + + public static synchronized void deleteExternalSignalingInstanceForUserEntity(long id) { + WebSocketInstance webSocketInstance; + if ((webSocketInstance = webSocketInstanceMap.get(id)) != null) { + if (webSocketInstance.isConnected()) { + webSocketInstance.sendBye(); + webSocketInstanceMap.remove(id); + } + } + } + + HelloOverallWebSocketMessage getAssembledHelloModel(User user, String ticket) { + int apiVersion = ApiUtils.getSignalingApiVersion(user, new int[]{ApiUtils.API_V3, 2, 1}); + + HelloOverallWebSocketMessage helloOverallWebSocketMessage = new HelloOverallWebSocketMessage(); + helloOverallWebSocketMessage.setType("hello"); + HelloWebSocketMessage helloWebSocketMessage = new HelloWebSocketMessage(); + helloWebSocketMessage.setVersion("1.0"); + AuthWebSocketMessage authWebSocketMessage = new AuthWebSocketMessage(); + authWebSocketMessage.setUrl(ApiUtils.getUrlForSignalingBackend(apiVersion, user.getBaseUrl())); + AuthParametersWebSocketMessage authParametersWebSocketMessage = new AuthParametersWebSocketMessage(); + authParametersWebSocketMessage.setTicket(ticket); + if (!("?").equals(user.getUserId())) { + authParametersWebSocketMessage.setUserid(user.getUserId()); + } + authWebSocketMessage.setAuthParametersWebSocketMessage(authParametersWebSocketMessage); + helloWebSocketMessage.setAuthWebSocketMessage(authWebSocketMessage); + helloOverallWebSocketMessage.setHelloWebSocketMessage(helloWebSocketMessage); + return helloOverallWebSocketMessage; + } + + HelloOverallWebSocketMessage getAssembledHelloModelForResume(String resumeId) { + HelloOverallWebSocketMessage helloOverallWebSocketMessage = new HelloOverallWebSocketMessage(); + helloOverallWebSocketMessage.setType("hello"); + HelloWebSocketMessage helloWebSocketMessage = new HelloWebSocketMessage(); + helloWebSocketMessage.setVersion("1.0"); + helloWebSocketMessage.setResumeid(resumeId); + helloOverallWebSocketMessage.setHelloWebSocketMessage(helloWebSocketMessage); + return helloOverallWebSocketMessage; + } + + RoomOverallWebSocketMessage getAssembledJoinOrLeaveRoomModel(String roomId, String sessionId, + FederationSettings federation) { + RoomOverallWebSocketMessage roomOverallWebSocketMessage = new RoomOverallWebSocketMessage(); + roomOverallWebSocketMessage.setType("room"); + RoomWebSocketMessage roomWebSocketMessage = new RoomWebSocketMessage(); + roomWebSocketMessage.setRoomId(roomId); + roomWebSocketMessage.setSessionId(sessionId); + if (federation != null) { + String federationAuthToken = null; + if (federation.getHelloAuthParams() != null) { + federationAuthToken = federation.getHelloAuthParams().getToken(); + } + RoomFederationWebSocketMessage roomFederationWebSocketMessage = new RoomFederationWebSocketMessage(); + roomFederationWebSocketMessage.setSignaling(federation.getServer()); + roomFederationWebSocketMessage.setUrl(federation.getNextcloudServer() + "/ocs/v2.php/apps/spreed/api/v3/signaling/backend"); + roomFederationWebSocketMessage.setRoomid(federation.getRoomId()); + roomFederationWebSocketMessage.setToken(federationAuthToken); + roomWebSocketMessage.setRoomFederationWebSocketMessage(roomFederationWebSocketMessage); + } + roomOverallWebSocketMessage.setRoomWebSocketMessage(roomWebSocketMessage); + return roomOverallWebSocketMessage; + } + + CallOverallWebSocketMessage getAssembledCallMessageModel(NCSignalingMessage ncSignalingMessage) { + CallOverallWebSocketMessage callOverallWebSocketMessage = new CallOverallWebSocketMessage(); + callOverallWebSocketMessage.setType("message"); + + CallWebSocketMessage callWebSocketMessage = new CallWebSocketMessage(); + + ActorWebSocketMessage actorWebSocketMessage = new ActorWebSocketMessage(); + actorWebSocketMessage.setType("session"); + actorWebSocketMessage.setSessionId(ncSignalingMessage.getTo()); + callWebSocketMessage.setRecipientWebSocketMessage(actorWebSocketMessage); + callWebSocketMessage.setNcSignalingMessage(ncSignalingMessage); + + callOverallWebSocketMessage.setCallWebSocketMessage(callWebSocketMessage); + return callOverallWebSocketMessage; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt new file mode 100644 index 0000000..81b7847 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt @@ -0,0 +1,497 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc + +import android.content.Context +import android.text.TextUtils +import android.util.Log +import autodagger.AutoInjector +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.events.NetworkEvent +import com.nextcloud.talk.events.WebSocketCommunicationEvent +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.models.json.participants.Participant.ActorType +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage +import com.nextcloud.talk.models.json.signaling.settings.FederationSettings +import com.nextcloud.talk.models.json.websocket.BaseWebSocketMessage +import com.nextcloud.talk.models.json.websocket.ByeWebSocketMessage +import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage +import com.nextcloud.talk.models.json.websocket.CallWebSocketMessage +import com.nextcloud.talk.models.json.websocket.ErrorOverallWebSocketMessage +import com.nextcloud.talk.models.json.websocket.EventOverallWebSocketMessage +import com.nextcloud.talk.models.json.websocket.HelloResponseOverallWebSocketMessage +import com.nextcloud.talk.models.json.websocket.JoinedRoomOverallWebSocketMessage +import com.nextcloud.talk.signaling.SignalingMessageReceiver +import com.nextcloud.talk.signaling.SignalingMessageSender +import com.nextcloud.talk.utils.bundle.BundleKeys +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.io.IOException +import java.lang.Thread.sleep +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +@Suppress("TooManyFunctions") +class WebSocketInstance internal constructor(conversationUser: User, connectionUrl: String, webSocketTicket: String) : + WebSocketListener() { + @JvmField + @Inject + var okHttpClient: OkHttpClient? = null + + @JvmField + @Inject + var eventBus: EventBus? = null + + @JvmField + @Inject + var context: Context? = null + private val conversationUser: User + private val webSocketTicket: String + private var resumeId: String? = null + var sessionId: String? = null + private set + private var hasMCU = false + var isConnected: Boolean + private set + private val webSocketConnectionHelper: WebSocketConnectionHelper + private var internalWebSocket: WebSocket? = null + private val connectionUrl: String + private var currentRoomToken: String? = null + private var currentNormalBackendSession: String? = null + private var currentFederation: FederationSettings? = null + private var reconnecting = false + private val usersHashMap: HashMap + private var messagesQueue: MutableList = ArrayList() + private val signalingMessageReceiver = ExternalSignalingMessageReceiver() + val signalingMessageSender = ExternalSignalingMessageSender() + + init { + sharedApplication!!.componentApplication.inject(this) + this.connectionUrl = connectionUrl + this.conversationUser = conversationUser + this.webSocketTicket = webSocketTicket + webSocketConnectionHelper = WebSocketConnectionHelper() + usersHashMap = HashMap() + isConnected = false + eventBus!!.register(this) + restartWebSocket() + } + + private fun sendHello() { + try { + if (TextUtils.isEmpty(resumeId)) { + internalWebSocket!!.send( + LoganSquare.serialize( + webSocketConnectionHelper + .getAssembledHelloModel(conversationUser, webSocketTicket) + ) + ) + } else { + internalWebSocket!!.send( + LoganSquare.serialize( + webSocketConnectionHelper + .getAssembledHelloModelForResume(resumeId) + ) + ) + } + } catch (e: IOException) { + Log.e(TAG, "Failed to serialize hello model") + } + } + + override fun onOpen(webSocket: WebSocket, response: Response) { + Log.d(TAG, "Open webSocket") + internalWebSocket = webSocket + sendHello() + } + + private fun closeWebSocket(webSocket: WebSocket) { + webSocket.close(NORMAL_CLOSURE, null) + webSocket.cancel() + if (webSocket === internalWebSocket) { + isConnected = false + messagesQueue = ArrayList() + } + sleep(ONE_SECOND) + restartWebSocket() + } + + fun clearResumeId() { + resumeId = "" + } + + fun restartWebSocket() { + reconnecting = true + Log.d(TAG, "restartWebSocket: $connectionUrl") + val request = Request.Builder().url(connectionUrl).build() + okHttpClient!!.newWebSocket(request, this) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + if (webSocket === internalWebSocket) { + Log.d(TAG, "Receiving : $webSocket $text") + try { + val (messageType) = LoganSquare.parse(text, BaseWebSocketMessage::class.java) + if (messageType != null) { + when (messageType) { + "hello" -> processHelloMessage(webSocket, text) + "error" -> processErrorMessage(webSocket, text) + "room" -> processJoinedRoomMessage(text) + "event" -> processEventMessage(text) + "message" -> processMessage(text) + "bye" -> { + isConnected = false + resumeId = "" + } + + else -> {} + } + } else { + Log.e(TAG, "Received message with type: null") + } + } catch (e: IOException) { + Log.e(TAG, "Failed to recognize WebSocket message", e) + } + } + } + + @Throws(IOException::class) + private fun processMessage(text: String) { + val (_, callWebSocketMessage) = LoganSquare.parse(text, CallOverallWebSocketMessage::class.java) + if (callWebSocketMessage != null) { + val ncSignalingMessage = callWebSocketMessage.ncSignalingMessage + + if (ncSignalingMessage != null && + TextUtils.isEmpty(ncSignalingMessage.from) && + callWebSocketMessage.senderWebSocketMessage != null + ) { + ncSignalingMessage.from = callWebSocketMessage.senderWebSocketMessage!!.sessionId + } + + signalingMessageReceiver.process(callWebSocketMessage) + } + } + + @Throws(IOException::class) + private fun processEventMessage(text: String) { + val eventOverallWebSocketMessage = LoganSquare.parse(text, EventOverallWebSocketMessage::class.java) + if (eventOverallWebSocketMessage.eventMap != null) { + val target = eventOverallWebSocketMessage.eventMap!!["target"] as String? + if (target != null) { + when (target) { + Globals.TARGET_ROOM -> { + if ("message" == eventOverallWebSocketMessage.eventMap!!["type"]) { + processRoomMessageMessage(eventOverallWebSocketMessage) + } else if ("join" == eventOverallWebSocketMessage.eventMap!!["type"]) { + processRoomJoinMessage(eventOverallWebSocketMessage) + } else if ("leave" == eventOverallWebSocketMessage.eventMap!!["type"]) { + processRoomLeaveMessage(eventOverallWebSocketMessage) + } + signalingMessageReceiver.process(eventOverallWebSocketMessage.eventMap) + } + + Globals.TARGET_PARTICIPANTS -> + signalingMessageReceiver.process(eventOverallWebSocketMessage.eventMap) + + else -> + Log.i(TAG, "Received unknown/ignored event target: $target") + } + } else { + Log.w(TAG, "Received message with event target: null") + } + } + } + + private fun processRoomMessageMessage(eventOverallWebSocketMessage: EventOverallWebSocketMessage) { + val messageHashMap = eventOverallWebSocketMessage.eventMap?.get("message") as Map<*, *>? + + if (messageHashMap != null && messageHashMap.containsKey("data")) { + val dataHashMap = messageHashMap["data"] as Map<*, *>? + + if (dataHashMap != null && dataHashMap.containsKey("chat")) { + val chatMap = dataHashMap["chat"] as Map<*, *>? + if (chatMap != null && chatMap.containsKey("refresh") && chatMap["refresh"] as Boolean) { + val refreshChatHashMap = HashMap() + refreshChatHashMap[BundleKeys.KEY_ROOM_TOKEN] = messageHashMap["roomid"] as String? + refreshChatHashMap[BundleKeys.KEY_INTERNAL_USER_ID] = (conversationUser.id!!).toString() + eventBus!!.post(WebSocketCommunicationEvent("refreshChat", refreshChatHashMap)) + } + } else if (dataHashMap != null && dataHashMap.containsKey("recording")) { + val recordingMap = dataHashMap["recording"] as Map<*, *>? + if (recordingMap != null && recordingMap.containsKey("status")) { + val status = (recordingMap["status"] as Long?)!!.toInt() + Log.d(TAG, "status is $status") + val recordingHashMap = HashMap() + recordingHashMap[BundleKeys.KEY_RECORDING_STATE] = status.toString() + eventBus!!.post(WebSocketCommunicationEvent("recordingStatus", recordingHashMap)) + } + } + } + } + + private fun processRoomJoinMessage(eventOverallWebSocketMessage: EventOverallWebSocketMessage) { + val joinEventList = eventOverallWebSocketMessage.eventMap?.get("join") as List>? + var internalHashMap: HashMap + var participant: Participant + for (i in joinEventList!!.indices) { + internalHashMap = joinEventList[i] + val userMap = internalHashMap["user"] as HashMap? + participant = Participant() + val userId = internalHashMap["userid"] as String? + if (userId != null) { + participant.actorType = ActorType.USERS + participant.actorId = userId + } else { + participant.actorType = ActorType.GUESTS + // FIXME seems to be not given by the HPB: participant.setActorId(); + } + if (userMap != null) { + // There is no "user" attribute for guest participants. + participant.displayName = userMap["displayname"] as String? + } + usersHashMap[internalHashMap["sessionid"] as String?] = participant + } + } + + private fun processRoomLeaveMessage(eventOverallWebSocketMessage: EventOverallWebSocketMessage) { + val leaveEventList = eventOverallWebSocketMessage.eventMap?.get("leave") as List? + for (i in leaveEventList!!.indices) { + usersHashMap.remove(leaveEventList[i]) + } + } + + fun getUserMap(): HashMap = usersHashMap + + @Throws(IOException::class) + private fun processJoinedRoomMessage(text: String) { + val (_, roomWebSocketMessage) = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage::class.java) + if (roomWebSocketMessage != null) { + currentRoomToken = roomWebSocketMessage.roomId + if (roomWebSocketMessage.roomPropertiesWebSocketMessage != null && !TextUtils.isEmpty(currentRoomToken)) { + sendRoomJoinedEvent() + } + } + } + + @Throws(IOException::class) + private fun processErrorMessage(webSocket: WebSocket, text: String) { + Log.e(TAG, "Received error: $text") + val (_, message) = LoganSquare.parse(text, ErrorOverallWebSocketMessage::class.java) + if (message != null) { + if ("no_such_session" == message.code) { + Log.d(TAG, "WebSocket " + webSocket.hashCode() + " resumeID " + resumeId + " expired") + resumeId = "" + currentRoomToken = "" + currentNormalBackendSession = "" + restartWebSocket() + } else if ("hello_expected" == message.code) { + restartWebSocket() + } + } + } + + @Throws(IOException::class) + private fun processHelloMessage(webSocket: WebSocket, text: String) { + isConnected = true + reconnecting = false + val oldResumeId = resumeId + val (_, helloResponseWebSocketMessage1) = LoganSquare.parse( + text, + HelloResponseOverallWebSocketMessage::class.java + ) + if (helloResponseWebSocketMessage1 != null) { + resumeId = helloResponseWebSocketMessage1.resumeId + sessionId = helloResponseWebSocketMessage1.sessionId + hasMCU = helloResponseWebSocketMessage1.serverHasMCUSupport() + } + for (i in messagesQueue.indices) { + webSocket.send(messagesQueue[i]) + } + messagesQueue = ArrayList() + val helloHashMap = HashMap() + if (!TextUtils.isEmpty(oldResumeId)) { + helloHashMap["oldResumeId"] = oldResumeId + } else { + currentRoomToken = "" + currentNormalBackendSession = "" + } + if (!TextUtils.isEmpty(currentRoomToken)) { + helloHashMap[Globals.ROOM_TOKEN] = currentRoomToken + } + eventBus!!.post(WebSocketCommunicationEvent("hello", helloHashMap)) + } + + private fun sendRoomJoinedEvent() { + val joinRoomHashMap = HashMap() + joinRoomHashMap[Globals.ROOM_TOKEN] = currentRoomToken + eventBus!!.post(WebSocketCommunicationEvent("roomJoined", joinRoomHashMap)) + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + Log.d(TAG, "Receiving bytes : " + bytes.hex()) + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + Log.d(TAG, "onClosing : $code / $reason") + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + Log.d(TAG, "onClosed : $code / $reason") + isConnected = false + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Log.e(TAG, "Error : WebSocket " + webSocket.hashCode(), t) + closeWebSocket(webSocket) + } + + fun hasMCU(): Boolean = hasMCU + + @Suppress("Detekt.ComplexMethod") + fun joinRoomWithRoomTokenAndSession( + roomToken: String, + normalBackendSession: String?, + federation: FederationSettings? = null + ) { + Log.d(TAG, "joinRoomWithRoomTokenAndSession") + Log.d(TAG, " roomToken: $roomToken") + Log.d(TAG, " session: $normalBackendSession") + try { + val message = LoganSquare.serialize( + webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken, normalBackendSession, federation) + ) + if (roomToken == "") { + Log.d(TAG, "sending 'leave room' via websocket") + currentNormalBackendSession = "" + currentFederation = null + sendMessage(message) + } else if ( + roomToken == currentRoomToken && + normalBackendSession == currentNormalBackendSession && + federation?.roomId == currentFederation?.roomId && + federation?.nextcloudServer == currentFederation?.nextcloudServer + ) { + Log.d(TAG, "roomToken & session are unchanged. Joining locally without to send websocket message") + sendRoomJoinedEvent() + } else { + Log.d(TAG, "Sending join room message via websocket") + currentNormalBackendSession = normalBackendSession + currentFederation = federation + sendMessage(message) + } + } catch (e: IOException) { + Log.e(TAG, "Failed to serialize signaling message", e) + } + } + + private fun sendCallMessage(ncSignalingMessage: NCSignalingMessage) { + try { + val message = LoganSquare.serialize( + webSocketConnectionHelper.getAssembledCallMessageModel(ncSignalingMessage) + ) + sendMessage(message) + } catch (e: IOException) { + Log.e(TAG, "Failed to serialize signaling message", e) + } + } + + private fun sendMessage(message: String) { + if (!isConnected || reconnecting) { + messagesQueue.add(message) + + if (!reconnecting) { + restartWebSocket() + } + } else { + if (!internalWebSocket!!.send(message)) { + messagesQueue.add(message) + restartWebSocket() + } + } + } + + fun sendBye() { + if (isConnected) { + try { + val byeWebSocketMessage = ByeWebSocketMessage() + byeWebSocketMessage.type = "bye" + byeWebSocketMessage.bye = HashMap() + internalWebSocket!!.send(LoganSquare.serialize(byeWebSocketMessage)) + } catch (e: IOException) { + Log.e(TAG, "Failed to serialize bye message") + } + } + } + + fun getDisplayNameForSession(session: String?): String? { + val participant = usersHashMap[session] + if (participant != null) { + if (participant.displayName != null) { + return participant.displayName + } + } + return "" + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(networkEvent: NetworkEvent) { + if (networkEvent.networkConnectionEvent == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED && + !isConnected + ) { + restartWebSocket() + } + } + + fun getSignalingMessageReceiver(): SignalingMessageReceiver = signalingMessageReceiver + + /** + * Temporary implementation of SignalingMessageReceiver until signaling related code is extracted to a Signaling + * class. + * + * + * All listeners are called in the WebSocket reader thread. This thread should be the same as long as the WebSocket + * stays connected, but it may change whenever it is connected again. + */ + private class ExternalSignalingMessageReceiver : SignalingMessageReceiver() { + fun process(eventMap: Map?) { + processEvent(eventMap) + } + + fun process(message: CallWebSocketMessage?) { + if (message?.ncSignalingMessage?.type == "startedTyping" || + message?.ncSignalingMessage?.type == "stoppedTyping" + ) { + processCallWebSocketMessage(message) + } else { + processSignalingMessage(message?.ncSignalingMessage) + } + } + } + + inner class ExternalSignalingMessageSender : SignalingMessageSender { + override fun send(ncSignalingMessage: NCSignalingMessage) { + sendCallMessage(ncSignalingMessage) + } + } + + companion object { + private const val TAG = "WebSocketInstance" + private const val NORMAL_CLOSURE = 1000 + private const val ONE_SECOND: Long = 1000 + } +} diff --git a/app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java b/app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java new file mode 100644 index 0000000..55fc5a9 --- /dev/null +++ b/app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java @@ -0,0 +1,296 @@ +/* + * Nominatim Java API client + * + * SPDX-FileCopyrightText: 2010 - 2014 Dudie + * SPDX-License-Identifier: LGPL-3.0-or-later + */ +package fr.dudie.nominatim.client; + +import android.util.Log; + +import com.github.filosganga.geogson.gson.GeometryAdapterFactory; +import com.github.filosganga.geogson.jts.JtsAdapterFactory; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; + +import fr.dudie.nominatim.client.request.NominatimLookupRequest; +import fr.dudie.nominatim.client.request.NominatimReverseRequest; +import fr.dudie.nominatim.client.request.NominatimSearchRequest; +import fr.dudie.nominatim.client.request.paramhelper.OsmType; +import fr.dudie.nominatim.gson.ArrayOfAddressElementsDeserializer; +import fr.dudie.nominatim.gson.ArrayOfPolygonPointsDeserializer; +import fr.dudie.nominatim.gson.BoundingBoxDeserializer; +import fr.dudie.nominatim.gson.PolygonPointDeserializer; +import fr.dudie.nominatim.model.Address; +import fr.dudie.nominatim.model.BoundingBox; +import fr.dudie.nominatim.model.Element; +import fr.dudie.nominatim.model.PolygonPoint; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * An implementation of the Nominatim Api Service. + * + * @author Jérémie Huchet + * @author Sunil D S + * @author Andy Scherzinger + */ +public final class TalkJsonNominatimClient implements NominatimClient { + private static final String TAG = "TalkNominationClient"; + + /** + * UTF-8 encoding. + */ + public static final String ENCODING_UTF_8 = "UTF-8"; + + private final OkHttpClient httpClient; + + /** + * Gson instance for Nominatim API calls. + */ + private final Gson gson; + + /** + * The url to make search queries. + */ + private final String searchUrl; + + /** + * The url for reverse geocoding. + */ + private final String reverseUrl; + + /** + * The url for address lookup. + */ + private final String lookupUrl; + + /** + * The default search options. + */ + private final NominatimOptions defaults; + + /** + * Creates the json nominatim client. + * + * @param baseUrl the nominatim server url + * @param httpClient an HTTP client + * @param email an email to add in the HTTP requests parameters to "sign" them (see + * https://wiki.openstreetmap.org/wiki/Nominatim_usage_policy) + */ + public TalkJsonNominatimClient(final String baseUrl, final OkHttpClient httpClient, final String email) { + this(baseUrl, httpClient, email, new NominatimOptions()); + } + + /** + * Creates the json nominatim client. + * + * @param baseUrl the nominatim server url + * @param httpClient an HTTP client + * @param email an email to add in the HTTP requests parameters to "sign" them (see + * https://wiki.openstreetmap.org/wiki/Nominatim_usage_policy) + * @param defaults defaults options, they override null valued requests options + */ + public TalkJsonNominatimClient(final String baseUrl, final OkHttpClient httpClient, final String email, final NominatimOptions defaults) { + String emailEncoded; + try { + emailEncoded = URLEncoder.encode(email, ENCODING_UTF_8); + } catch (UnsupportedEncodingException e) { + emailEncoded = email; + } + this.searchUrl = String.format("%s/search?format=jsonv2&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded); + this.reverseUrl = String.format("%s/reverse?format=jsonv2&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded); + this.lookupUrl = String.format("%s/lookup?format=json&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded); + + Log.d(TAG, "API search URL: " + searchUrl); + Log.d(TAG, "API reverse URL: " + reverseUrl); + + this.defaults = defaults; + + // prepare gson instance + final GsonBuilder gsonBuilder = new GsonBuilder(); + + gsonBuilder.registerTypeAdapter(Element[].class, new ArrayOfAddressElementsDeserializer()); + gsonBuilder.registerTypeAdapter(PolygonPoint.class, new PolygonPointDeserializer()); + gsonBuilder.registerTypeAdapter(PolygonPoint[].class, new ArrayOfPolygonPointsDeserializer()); + gsonBuilder.registerTypeAdapter(BoundingBox.class, new BoundingBoxDeserializer()); + + gsonBuilder.registerTypeAdapterFactory(new JtsAdapterFactory()); + gsonBuilder.registerTypeAdapterFactory(new GeometryAdapterFactory()); + + gson = gsonBuilder.create(); + + // prepare httpclient + this.httpClient = httpClient; + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#search(fr.dudie.nominatim.client.request.NominatimSearchRequest) + */ + @Override + public List

search(final NominatimSearchRequest search) throws IOException { + + defaults.mergeTo(search); + final String apiCall = String.format("%s&%s", searchUrl, search.getQueryString()); + Log.d(TAG, "search url: " + apiCall); + + Request requesthttp = new Request.Builder() + .addHeader("accept", "application/json") + .url(apiCall) + .build(); + + Response response = httpClient.newCall(requesthttp).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + return gson.fromJson(responseBody.string(), new TypeToken>() { + }.getType()); + } + } + + return new ArrayList<>(); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#getAddress(fr.dudie.nominatim.client.request.NominatimReverseRequest) + */ + @Override + public Address getAddress(final NominatimReverseRequest reverse) throws IOException { + + final String apiCall = String.format("%s&%s", reverseUrl, reverse.getQueryString()); + Log.d(TAG, "reverse geocoding url: " + apiCall); + + Request requesthttp = new Request.Builder() + .addHeader("accept", "application/json") + .url(apiCall) + .build(); + + Response response = httpClient.newCall(requesthttp).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + return gson.fromJson(responseBody.string(), Address.class); + } + } + + return null; + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#lookupAddress(fr.dudie.nominatim.client.request.NominatimLookupRequest) + */ + @Override + public List
lookupAddress(final NominatimLookupRequest lookup) throws IOException { + + final String apiCall = String.format("%s&%s", lookupUrl, lookup.getQueryString()); + Log.d(TAG, "lookup url: " + apiCall); + Request requesthttp = new Request.Builder() + .addHeader("accept", "application/json") + .url(apiCall) + .build(); + + Response response = httpClient.newCall(requesthttp).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + return gson.fromJson(responseBody.string(), new TypeToken>() { + }.getType()); + } + } + + return new ArrayList<>(); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#search(java.lang.String) + */ + @Override + public List
search(final String query) throws IOException { + + final NominatimSearchRequest q = new NominatimSearchRequest(); + q.setQuery(query); + return this.search(q); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#getAddress(double, double) + */ + @Override + public Address getAddress(final double longitude, final double latitude) throws IOException { + + final NominatimReverseRequest q = new NominatimReverseRequest(); + q.setQuery(longitude, latitude); + return this.getAddress(q); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#getAddress(double, double, int) + */ + @Override + public Address getAddress(final double longitude, final double latitude, final int zoom) + throws IOException { + + final NominatimReverseRequest q = new NominatimReverseRequest(); + q.setQuery(longitude, latitude); + q.setZoom(zoom); + return this.getAddress(q); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#getAddress(int, int) + */ + @Override + public Address getAddress(final int longitudeE6, final int latitudeE6) throws IOException { + + return this.getAddress((longitudeE6 / 1E6), (latitudeE6 / 1E6)); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#getAddress(String, long) + */ + @Override + public Address getAddress(final String type, final long id) throws IOException { + + final NominatimReverseRequest q = new NominatimReverseRequest(); + q.setQuery(OsmType.from(type), id); + return this.getAddress(q); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#lookupAddress(java.util.List) + */ + @Override + public List
lookupAddress(final List typeId) throws IOException { + + final NominatimLookupRequest q = new NominatimLookupRequest(); + q.setQuery(typeId); + return this.lookupAddress(q); + } +} diff --git a/app/src/main/java/third/parties/daveKoeller/AlphanumComparator.java b/app/src/main/java/third/parties/daveKoeller/AlphanumComparator.java new file mode 100644 index 0000000..4c9d037 --- /dev/null +++ b/app/src/main/java/third/parties/daveKoeller/AlphanumComparator.java @@ -0,0 +1,170 @@ +/* + * AlphanumComparator + * + * SPDX-FileCopyrightText: 2012 Dave Koelle + * SPDX-FileCopyrightText: 2012 Daniel Migowski + * SPDX-FileCopyrightText: 2012 Andre Bogus + * SPDX-License-Identifier: LGPL-2.1-or-later + */ +package third.parties.daveKoeller; + +import java.io.Serializable; +import java.math.BigInteger; +import java.text.Collator; +import java.util.Comparator; + +/* + * The Alphanum Algorithm is an improved sorting algorithm for strings + * containing numbers. Instead of sorting numbers in ASCII order like + * a standard sort, this algorithm sorts numbers in numeric order. + * + * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com + * + * This is an updated version with enhancements made by Daniel Migowski, Andre Bogus, and David Koelle + * * + * To convert to use Templates (Java 1.5+): + * - Change "implements Comparator" to "implements Comparator" + * - Change "compare(Object o1, Object o2)" to "compare(String s1, String s2)" + * - Remove the type checking and casting in compare(). + * + * To use this class: + * Use the static "sort" method from the java.util.Collections class: + * Collections.sort(your list, new AlphanumComparator()); + * + * Adapted to fit + * https://github.com/nextcloud/server/blob/9a4253ef7c34f9dc71a6a9f7828a10df769f0c32/tests/lib/NaturalSortTest.php + * by Tobias Kaminsky + */ +public class AlphanumComparator implements Comparator, Serializable { + private boolean isDigit(char ch) { + return ch >= 48 && ch <= 57; + } + + private boolean isSpecialChar(char ch) { + return ch <= 47 || ch >= 58 && ch <= 64 || ch >= 91 && ch <= 96 || ch >= 123 && ch <= 126; + } + + /** + * Length of string is passed in for improved efficiency (only need to calculate it once) + **/ + private String getChunk(String string, int stringLength, int marker) { + StringBuilder chunk = new StringBuilder(); + char c = string.charAt(marker); + chunk.append(c); + marker++; + if (isDigit(c)) { + while (marker < stringLength) { + c = string.charAt(marker); + if (!isDigit(c)) { + break; + } + chunk.append(c); + marker++; + } + } else if (!isSpecialChar(c)) { + while (marker < stringLength) { + c = string.charAt(marker); + if (isDigit(c) || isSpecialChar(c)) { + break; + } + chunk.append(c); + marker++; + } + } + return chunk.toString(); + } + + public int compare(T t1, T t2) { + return compare(t1.toString(), t2.toString()); + } + + public int compare(String s1, String s2) { + int thisMarker = 0; + int thatMarker = 0; + int s1Length = s1.length(); + int s2Length = s2.length(); + + while (thisMarker < s1Length && thatMarker < s2Length) { + String thisChunk = getChunk(s1, s1Length, thisMarker); + thisMarker += thisChunk.length(); + + String thatChunk = getChunk(s2, s2Length, thatMarker); + thatMarker += thatChunk.length(); + + // If both chunks contain numeric characters, sort them numerically + int result = 0; + if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) { + // extract digits + int thisChunkZeroCount = 0; + boolean zero = true; + int countThis = 0; + while (countThis < (thisChunk.length()) && isDigit(thisChunk.charAt(countThis))) { + if (zero) { + if (Character.getNumericValue(thisChunk.charAt(countThis)) == 0) { + thisChunkZeroCount++; + } else { + zero = false; + } + } + countThis++; + } + + + int thatChunkZeroCount = 0; + int countThat = 0; + zero = true; + while (countThat < (thatChunk.length()) && isDigit(thatChunk.charAt(countThat))) { + if (zero) { + if (Character.getNumericValue(thatChunk.charAt(countThat)) == 0) { + thatChunkZeroCount++; + } else { + zero = false; + } + } + countThat++; + } + + BigInteger thisChunkValue = new BigInteger(thisChunk.substring(0, countThis)); + BigInteger thatChunkValue = new BigInteger(thatChunk.substring(0, countThat)); + + result = thisChunkValue.compareTo(thatChunkValue); + + if (result == 0) { + // value is equal, compare leading zeros + result = Integer.compare(thisChunkZeroCount, thatChunkZeroCount); + + if (result != 0) { + return result; + } + } else { + return result; + } + } else if (isSpecialChar(thisChunk.charAt(0)) && isSpecialChar(thatChunk.charAt(0))) { + for (int i = 0; i < thisChunk.length(); i++) { + if (thisChunk.charAt(i) == '.' && thatChunk.charAt(i) != '.') { + return -1; + } else if (thatChunk.charAt(i) == '.' && thisChunk.charAt(i) != '.') { + return 1; + } else { + result = thisChunk.charAt(i) - thatChunk.charAt(i); + if (result != 0) { + return result; + } + } + } + } else if (isSpecialChar(thisChunk.charAt(0)) && !isSpecialChar(thatChunk.charAt(0))) { + return -1; + } else if (!isSpecialChar(thisChunk.charAt(0)) && isSpecialChar(thatChunk.charAt(0))) { + return 1; + } else { + result = Collator.getInstance().compare(thisChunk, thatChunk); + } + + if (result != 0) { + return result; + } + } + + return s1Length - s2Length; + } +} diff --git a/app/src/main/java/third/parties/fresco/BetterImageSpan.kt b/app/src/main/java/third/parties/fresco/BetterImageSpan.kt new file mode 100644 index 0000000..de00d6b --- /dev/null +++ b/app/src/main/java/third/parties/fresco/BetterImageSpan.kt @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2015-present, Facebook, Inc. and its affiliates. + * SPDX-License-Identifier: MIT + */ +package third.parties.fresco + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Paint.FontMetricsInt +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.text.style.ReplacementSpan +import androidx.annotation.IntDef + +/** + * A better implementation of image spans that also supports centering images against the text. + * + * In order to migrate from ImageSpan, replace `new ImageSpan(drawable, alignment)` with + * `new BetterImageSpan(drawable, BetterImageSpan.normalizeAlignment(alignment))`. + * + * There are 2 main differences between BetterImageSpan and ImageSpan: + * 1. Pass in ALIGN_CENTER to center images against the text. + * 2. ALIGN_BOTTOM no longer unnecessarily increases the size of the text: + * DynamicDrawableSpan (ImageSpan's parent) adjusts sizes as if alignment was ALIGN_BASELINE + * which can lead to unnecessary whitespace. + */ +open class BetterImageSpan @JvmOverloads constructor( + val drawable: Drawable, + @param:BetterImageSpanAlignment private val mAlignment: Int = ALIGN_BASELINE +) : ReplacementSpan() { + @IntDef(*[ALIGN_BASELINE, ALIGN_BOTTOM, ALIGN_CENTER]) + @Retention(AnnotationRetention.SOURCE) + annotation class BetterImageSpanAlignment + + private var mWidth = 0 + private var mHeight = 0 + private var mBounds: Rect? = null + private val mFontMetricsInt = FontMetricsInt() + + init { + updateBounds() + } + + /** + * Returns the width of the image span and increases the height if font metrics are available. + */ + override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fontMetrics: FontMetricsInt?): Int { + updateBounds() + if (fontMetrics == null) { + return mWidth + } + val offsetAbove = getOffsetAboveBaseline(fontMetrics) + val offsetBelow = mHeight + offsetAbove + if (offsetAbove < fontMetrics.ascent) { + fontMetrics.ascent = offsetAbove + } + if (offsetAbove < fontMetrics.top) { + fontMetrics.top = offsetAbove + } + if (offsetBelow > fontMetrics.descent) { + fontMetrics.descent = offsetBelow + } + if (offsetBelow > fontMetrics.bottom) { + fontMetrics.bottom = offsetBelow + } + return mWidth + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint + ) { + paint.getFontMetricsInt(mFontMetricsInt) + val iconTop = y + getOffsetAboveBaseline(mFontMetricsInt) + canvas.translate(x, iconTop.toFloat()) + drawable.draw(canvas) + canvas.translate(-x, -iconTop.toFloat()) + } + + private fun updateBounds() { + mBounds = drawable.bounds + mWidth = mBounds!!.width() + mHeight = mBounds!!.height() + } + + private fun getOffsetAboveBaseline(fm: FontMetricsInt): Int = + when (mAlignment) { + ALIGN_BOTTOM -> fm.descent - mHeight + ALIGN_CENTER -> { + val textHeight = fm.descent - fm.ascent + val offset = (textHeight - mHeight) / 2 + fm.ascent + offset + } + ALIGN_BASELINE -> -mHeight + else -> -mHeight + } + + companion object { + const val ALIGN_BOTTOM = 0 + const val ALIGN_BASELINE = 1 + const val ALIGN_CENTER = 2 + } +} diff --git a/app/src/main/res/anim/popup_animation.xml b/app/src/main/res/anim/popup_animation.xml new file mode 100644 index 0000000..246ce1b --- /dev/null +++ b/app/src/main/res/anim/popup_animation.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/appbar_elevation_off.xml b/app/src/main/res/animator/appbar_elevation_off.xml new file mode 100644 index 0000000..cafe2f2 --- /dev/null +++ b/app/src/main/res/animator/appbar_elevation_off.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/animator/appbar_elevation_on.xml b/app/src/main/res/animator/appbar_elevation_on.xml new file mode 100644 index 0000000..75ec616 --- /dev/null +++ b/app/src/main/res/animator/appbar_elevation_on.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/drawable-land/link_text_background.xml b/app/src/main/res/drawable-land/link_text_background.xml new file mode 100644 index 0000000..9825cef --- /dev/null +++ b/app/src/main/res/drawable-land/link_text_background.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/account_circle_48dp.xml b/app/src/main/res/drawable-night/account_circle_48dp.xml new file mode 100644 index 0000000..97bce5e --- /dev/null +++ b/app/src/main/res/drawable-night/account_circle_48dp.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable-night/account_circle_96dp.xml b/app/src/main/res/drawable-night/account_circle_96dp.xml new file mode 100644 index 0000000..48636b3 --- /dev/null +++ b/app/src/main/res/drawable-night/account_circle_96dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable-night/ic_cellphone.xml b/app/src/main/res/drawable-night/ic_cellphone.xml new file mode 100644 index 0000000..f3fef84 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_cellphone.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable-night/ic_chevron_right.xml b/app/src/main/res/drawable-night/ic_chevron_right.xml new file mode 100644 index 0000000..fe2f206 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_chevron_right.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable-night/ic_circular_document.xml b/app/src/main/res/drawable-night/ic_circular_document.xml new file mode 100644 index 0000000..940ff39 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_circular_document.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/drawable-night/ic_circular_group.xml b/app/src/main/res/drawable-night/ic_circular_group.xml new file mode 100644 index 0000000..cbf33b1 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_circular_group.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable-night/ic_circular_group_mentions.xml b/app/src/main/res/drawable-night/ic_circular_group_mentions.xml new file mode 100644 index 0000000..7eb2d6d --- /dev/null +++ b/app/src/main/res/drawable-night/ic_circular_group_mentions.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_circular_link.xml b/app/src/main/res/drawable-night/ic_circular_link.xml new file mode 100644 index 0000000..2e1a699 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_circular_link.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable-night/ic_circular_location.xml b/app/src/main/res/drawable-night/ic_circular_location.xml new file mode 100644 index 0000000..c03c5a0 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_circular_location.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable-night/ic_circular_lock.xml b/app/src/main/res/drawable-night/ic_circular_lock.xml new file mode 100644 index 0000000..2a84bd6 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_circular_lock.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/app/src/main/res/drawable-night/ic_circular_mail.xml b/app/src/main/res/drawable-night/ic_circular_mail.xml new file mode 100644 index 0000000..4075aca --- /dev/null +++ b/app/src/main/res/drawable-night/ic_circular_mail.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable-night/ic_close_search.xml b/app/src/main/res/drawable-night/ic_close_search.xml new file mode 100644 index 0000000..6693881 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_close_search.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable-night/ic_contacts.xml b/app/src/main/res/drawable-night/ic_contacts.xml new file mode 100644 index 0000000..1a4fb4e --- /dev/null +++ b/app/src/main/res/drawable-night/ic_contacts.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable-night/ic_link.xml b/app/src/main/res/drawable-night/ic_link.xml new file mode 100644 index 0000000..3cbdd10 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_link.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable-night/ic_password.xml b/app/src/main/res/drawable-night/ic_password.xml new file mode 100644 index 0000000..6dee90f --- /dev/null +++ b/app/src/main/res/drawable-night/ic_password.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable-night/icon_circular_phone.xml b/app/src/main/res/drawable-night/icon_circular_phone.xml new file mode 100644 index 0000000..a11bd49 --- /dev/null +++ b/app/src/main/res/drawable-night/icon_circular_phone.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable-night/icon_circular_team.xml b/app/src/main/res/drawable-night/icon_circular_team.xml new file mode 100644 index 0000000..29cec1b --- /dev/null +++ b/app/src/main/res/drawable-night/icon_circular_team.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/accent_circle.xml b/app/src/main/res/drawable/accent_circle.xml new file mode 100644 index 0000000..1893213 --- /dev/null +++ b/app/src/main/res/drawable/accent_circle.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/account_circle_48dp.xml b/app/src/main/res/drawable/account_circle_48dp.xml new file mode 100644 index 0000000..d90f59f --- /dev/null +++ b/app/src/main/res/drawable/account_circle_48dp.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/account_circle_96dp.xml b/app/src/main/res/drawable/account_circle_96dp.xml new file mode 100644 index 0000000..7a6126c --- /dev/null +++ b/app/src/main/res/drawable/account_circle_96dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_article_24.xml b/app/src/main/res/drawable/baseline_article_24.xml new file mode 100644 index 0000000..cde492e --- /dev/null +++ b/app/src/main/res/drawable/baseline_article_24.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/baseline_audiotrack_24.xml b/app/src/main/res/drawable/baseline_audiotrack_24.xml new file mode 100644 index 0000000..4055a1d --- /dev/null +++ b/app/src/main/res/drawable/baseline_audiotrack_24.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/baseline_bar_chart_24.xml b/app/src/main/res/drawable/baseline_bar_chart_24.xml new file mode 100644 index 0000000..b9225c7 --- /dev/null +++ b/app/src/main/res/drawable/baseline_bar_chart_24.xml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/baseline_block_24.xml b/app/src/main/res/drawable/baseline_block_24.xml new file mode 100644 index 0000000..de57c88 --- /dev/null +++ b/app/src/main/res/drawable/baseline_block_24.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_calendar_month_24.xml b/app/src/main/res/drawable/baseline_calendar_month_24.xml new file mode 100644 index 0000000..f679c07 --- /dev/null +++ b/app/src/main/res/drawable/baseline_calendar_month_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_calendar_today_24.xml b/app/src/main/res/drawable/baseline_calendar_today_24.xml new file mode 100644 index 0000000..5a569b8 --- /dev/null +++ b/app/src/main/res/drawable/baseline_calendar_today_24.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml b/app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml new file mode 100644 index 0000000..44b5478 --- /dev/null +++ b/app/src/main/res/drawable/baseline_chat_bubble_outline_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_contacts_24.xml b/app/src/main/res/drawable/baseline_contacts_24.xml new file mode 100644 index 0000000..0640ee6 --- /dev/null +++ b/app/src/main/res/drawable/baseline_contacts_24.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/baseline_download_24.xml b/app/src/main/res/drawable/baseline_download_24.xml new file mode 100644 index 0000000..ffb255f --- /dev/null +++ b/app/src/main/res/drawable/baseline_download_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_error_24.xml b/app/src/main/res/drawable/baseline_error_24.xml new file mode 100644 index 0000000..2d42829 --- /dev/null +++ b/app/src/main/res/drawable/baseline_error_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_error_outline_24.xml b/app/src/main/res/drawable/baseline_error_outline_24.xml new file mode 100644 index 0000000..9c222c2 --- /dev/null +++ b/app/src/main/res/drawable/baseline_error_outline_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_format_list_bulleted_24.xml b/app/src/main/res/drawable/baseline_format_list_bulleted_24.xml new file mode 100644 index 0000000..6eda887 --- /dev/null +++ b/app/src/main/res/drawable/baseline_format_list_bulleted_24.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_image_24.xml b/app/src/main/res/drawable/baseline_image_24.xml new file mode 100644 index 0000000..7946c63 --- /dev/null +++ b/app/src/main/res/drawable/baseline_image_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_info_24.xml b/app/src/main/res/drawable/baseline_info_24.xml new file mode 100644 index 0000000..cd4dceb --- /dev/null +++ b/app/src/main/res/drawable/baseline_info_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_insert_drive_file_24.xml b/app/src/main/res/drawable/baseline_insert_drive_file_24.xml new file mode 100644 index 0000000..401ff6f --- /dev/null +++ b/app/src/main/res/drawable/baseline_insert_drive_file_24.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_location_pin_24.xml b/app/src/main/res/drawable/baseline_location_pin_24.xml new file mode 100644 index 0000000..886842d --- /dev/null +++ b/app/src/main/res/drawable/baseline_location_pin_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_lock_open_24.xml b/app/src/main/res/drawable/baseline_lock_open_24.xml new file mode 100644 index 0000000..ecc374e --- /dev/null +++ b/app/src/main/res/drawable/baseline_lock_open_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_mic_24.xml b/app/src/main/res/drawable/baseline_mic_24.xml new file mode 100644 index 0000000..53f4bb6 --- /dev/null +++ b/app/src/main/res/drawable/baseline_mic_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_notifications_24.xml b/app/src/main/res/drawable/baseline_notifications_24.xml new file mode 100644 index 0000000..c5e2041 --- /dev/null +++ b/app/src/main/res/drawable/baseline_notifications_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_photo_library_24.xml b/app/src/main/res/drawable/baseline_photo_library_24.xml new file mode 100644 index 0000000..19bdb85 --- /dev/null +++ b/app/src/main/res/drawable/baseline_photo_library_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_report_problem_24.xml b/app/src/main/res/drawable/baseline_report_problem_24.xml new file mode 100644 index 0000000..8525ebc --- /dev/null +++ b/app/src/main/res/drawable/baseline_report_problem_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_schedule_24.xml b/app/src/main/res/drawable/baseline_schedule_24.xml new file mode 100644 index 0000000..fb56fd0 --- /dev/null +++ b/app/src/main/res/drawable/baseline_schedule_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_stop_24.xml b/app/src/main/res/drawable/baseline_stop_24.xml new file mode 100644 index 0000000..b60513a --- /dev/null +++ b/app/src/main/res/drawable/baseline_stop_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_tag_faces_24.xml b/app/src/main/res/drawable/baseline_tag_faces_24.xml new file mode 100644 index 0000000..3a4f458 --- /dev/null +++ b/app/src/main/res/drawable/baseline_tag_faces_24.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/drawable/baseline_unfold_less_24.xml b/app/src/main/res/drawable/baseline_unfold_less_24.xml new file mode 100644 index 0000000..fb51bec --- /dev/null +++ b/app/src/main/res/drawable/baseline_unfold_less_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_unfold_more_24.xml b/app/src/main/res/drawable/baseline_unfold_more_24.xml new file mode 100644 index 0000000..7e336f3 --- /dev/null +++ b/app/src/main/res/drawable/baseline_unfold_more_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/baseline_video_24.xml b/app/src/main/res/drawable/baseline_video_24.xml new file mode 100644 index 0000000..d265c56 --- /dev/null +++ b/app/src/main/res/drawable/baseline_video_24.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/drawable/borderless_btn.xml b/app/src/main/res/drawable/borderless_btn.xml new file mode 100644 index 0000000..08f0b4e --- /dev/null +++ b/app/src/main/res/drawable/borderless_btn.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/current_location_circle.xml b/app/src/main/res/drawable/current_location_circle.xml new file mode 100644 index 0000000..7863b6f --- /dev/null +++ b/app/src/main/res/drawable/current_location_circle.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/cutout_circle.xml b/app/src/main/res/drawable/cutout_circle.xml new file mode 100644 index 0000000..c48a09e --- /dev/null +++ b/app/src/main/res/drawable/cutout_circle.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/deck.xml b/app/src/main/res/drawable/deck.xml new file mode 100644 index 0000000..56f42bf --- /dev/null +++ b/app/src/main/res/drawable/deck.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/forward_24.xml b/app/src/main/res/drawable/forward_24.xml new file mode 100644 index 0000000..6da3fec --- /dev/null +++ b/app/src/main/res/drawable/forward_24.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_account_plus.xml b/app/src/main/res/drawable/ic_account_plus.xml new file mode 100644 index 0000000..481e0e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_account_plus.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_add_grey600_24px.xml b/app/src/main/res/drawable/ic_add_grey600_24px.xml new file mode 100644 index 0000000..07059fc --- /dev/null +++ b/app/src/main/res/drawable/ic_add_grey600_24px.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_alphabetical_asc.xml b/app/src/main/res/drawable/ic_alphabetical_asc.xml new file mode 100644 index 0000000..7bc293e --- /dev/null +++ b/app/src/main/res/drawable/ic_alphabetical_asc.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_alphabetical_desc.xml b/app/src/main/res/drawable/ic_alphabetical_desc.xml new file mode 100644 index 0000000..b675320 --- /dev/null +++ b/app/src/main/res/drawable/ic_alphabetical_desc.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml new file mode 100644 index 0000000..12b9663 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_black_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_arrow_forward_white_24px.xml b/app/src/main/res/drawable/ic_arrow_forward_white_24px.xml new file mode 100644 index 0000000..c945294 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_forward_white_24px.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_av_timer_timer_24dp.xml b/app/src/main/res/drawable/ic_av_timer_timer_24dp.xml new file mode 100644 index 0000000..1283293 --- /dev/null +++ b/app/src/main/res/drawable/ic_av_timer_timer_24dp.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_avatar_background.xml b/app/src/main/res/drawable/ic_avatar_background.xml new file mode 100644 index 0000000..7959dac --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_background.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_document.xml b/app/src/main/res/drawable/ic_avatar_document.xml new file mode 100644 index 0000000..2644244 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_document.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/ic_avatar_federation.xml b/app/src/main/res/drawable/ic_avatar_federation.xml new file mode 100644 index 0000000..221b34e --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_federation.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_avatar_group.xml b/app/src/main/res/drawable/ic_avatar_group.xml new file mode 100644 index 0000000..0cf9d0f --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_group.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_avatar_group_small.xml b/app/src/main/res/drawable/ic_avatar_group_small.xml new file mode 100644 index 0000000..319ecd2 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_group_small.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_link.xml b/app/src/main/res/drawable/ic_avatar_link.xml new file mode 100644 index 0000000..efec69d --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_link.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_avatar_mail.xml b/app/src/main/res/drawable/ic_avatar_mail.xml new file mode 100644 index 0000000..7b721e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_mail.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_avatar_team_small.xml b/app/src/main/res/drawable/ic_avatar_team_small.xml new file mode 100644 index 0000000..4b40401 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_team_small.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_arrow_downward_24px.xml b/app/src/main/res/drawable/ic_baseline_arrow_downward_24px.xml new file mode 100644 index 0000000..af138b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_downward_24px.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_attach_file_24.xml b/app/src/main/res/drawable/ic_baseline_attach_file_24.xml new file mode 100644 index 0000000..2aa6e72 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_attach_file_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_bar_chart_24.xml b/app/src/main/res/drawable/ic_baseline_bar_chart_24.xml new file mode 100644 index 0000000..6128f72 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_bar_chart_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_bluetooth_audio_24.xml b/app/src/main/res/drawable/ic_baseline_bluetooth_audio_24.xml new file mode 100644 index 0000000..dfcf938 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_bluetooth_audio_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_close_24.xml b/app/src/main/res/drawable/ic_baseline_close_24.xml new file mode 100644 index 0000000..5bdf4c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_close_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_deck_24.xml b/app/src/main/res/drawable/ic_baseline_deck_24.xml new file mode 100644 index 0000000..ea4cbb8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_deck_24.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_do_not_touch_24.xml b/app/src/main/res/drawable/ic_baseline_do_not_touch_24.xml new file mode 100644 index 0000000..bdc6e8e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_do_not_touch_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_error_outline_24dp.xml b/app/src/main/res/drawable/ic_baseline_error_outline_24dp.xml new file mode 100644 index 0000000..4e4e62f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_error_outline_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_filter_list_24.xml b/app/src/main/res/drawable/ic_baseline_filter_list_24.xml new file mode 100644 index 0000000..cfb22b7 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_filter_list_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_flash_off_24.xml b/app/src/main/res/drawable/ic_baseline_flash_off_24.xml new file mode 100644 index 0000000..360917b --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_flash_off_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_flash_on_24.xml b/app/src/main/res/drawable/ic_baseline_flash_on_24.xml new file mode 100644 index 0000000..9e2d032 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_flash_on_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_flip_camera_android_24.xml b/app/src/main/res/drawable/ic_baseline_flip_camera_android_24.xml new file mode 100644 index 0000000..fb1105f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_flip_camera_android_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml b/app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml new file mode 100644 index 0000000..08aee2d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_headset_mic_24.xml b/app/src/main/res/drawable/ic_baseline_headset_mic_24.xml new file mode 100644 index 0000000..2307663 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_headset_mic_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_keyboard_24.xml b/app/src/main/res/drawable/ic_baseline_keyboard_24.xml new file mode 100644 index 0000000..4010709 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_keyboard_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_location_on_24.xml b/app/src/main/res/drawable/ic_baseline_location_on_24.xml new file mode 100644 index 0000000..0c6ceb7 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_location_on_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_location_on_red_24.xml b/app/src/main/res/drawable/ic_baseline_location_on_red_24.xml new file mode 100644 index 0000000..e681914 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_location_on_red_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_mic_24.xml b/app/src/main/res/drawable/ic_baseline_mic_24.xml new file mode 100644 index 0000000..750e8e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_mic_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_mic_red_24.xml b/app/src/main/res/drawable/ic_baseline_mic_red_24.xml new file mode 100644 index 0000000..21d6ca9 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_mic_red_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_notifications_off_24.xml b/app/src/main/res/drawable/ic_baseline_notifications_off_24.xml new file mode 100644 index 0000000..55b3ae3 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_notifications_off_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_pause_voice_message_24.xml b/app/src/main/res/drawable/ic_baseline_pause_voice_message_24.xml new file mode 100644 index 0000000..67c705d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_pause_voice_message_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_person_24.xml b/app/src/main/res/drawable/ic_baseline_person_24.xml new file mode 100644 index 0000000..bbe9cd2 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_person_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_phone_in_talk_24.xml b/app/src/main/res/drawable/ic_baseline_phone_in_talk_24.xml new file mode 100644 index 0000000..b24c77d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_phone_in_talk_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_phone_missed_24.xml b/app/src/main/res/drawable/ic_baseline_phone_missed_24.xml new file mode 100644 index 0000000..a0ddc7c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_phone_missed_24.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml b/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml new file mode 100644 index 0000000..6bc2340 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_picture_in_picture_alt_24.xml b/app/src/main/res/drawable/ic_baseline_picture_in_picture_alt_24.xml new file mode 100644 index 0000000..ee23aa0 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_picture_in_picture_alt_24.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_play_arrow_voice_message_24.xml b/app/src/main/res/drawable/ic_baseline_play_arrow_voice_message_24.xml new file mode 100644 index 0000000..83e1c74 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_play_arrow_voice_message_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_translate_24.xml b/app/src/main/res/drawable/ic_baseline_translate_24.xml new file mode 100644 index 0000000..8534fe4 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_translate_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_videocam_24.xml b/app/src/main/res/drawable/ic_baseline_videocam_24.xml new file mode 100644 index 0000000..f4f69c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_videocam_24.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_call_black_24dp.xml b/app/src/main/res/drawable/ic_call_black_24dp.xml new file mode 100644 index 0000000..e0ad769 --- /dev/null +++ b/app/src/main/res/drawable/ic_call_black_24dp.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_call_end_white_24px.xml b/app/src/main/res/drawable/ic_call_end_white_24px.xml new file mode 100644 index 0000000..76fce9b --- /dev/null +++ b/app/src/main/res/drawable/ic_call_end_white_24px.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_call_grey_600_24dp.xml b/app/src/main/res/drawable/ic_call_grey_600_24dp.xml new file mode 100644 index 0000000..e0d4df5 --- /dev/null +++ b/app/src/main/res/drawable/ic_call_grey_600_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_call_white_24dp.xml b/app/src/main/res/drawable/ic_call_white_24dp.xml new file mode 100644 index 0000000..44f1be4 --- /dev/null +++ b/app/src/main/res/drawable/ic_call_white_24dp.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cancel_black_24dp.xml b/app/src/main/res/drawable/ic_cancel_black_24dp.xml new file mode 100644 index 0000000..79edb24 --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel_black_24dp.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cancel_white_24dp.xml b/app/src/main/res/drawable/ic_cancel_white_24dp.xml new file mode 100644 index 0000000..9e46f05 --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel_white_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cellphone.xml b/app/src/main/res/drawable/ic_cellphone.xml new file mode 100644 index 0000000..774b2f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_cellphone.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..418544b --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_check_24.xml b/app/src/main/res/drawable/ic_check_24.xml new file mode 100644 index 0000000..63e0231 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_check_all.xml b/app/src/main/res/drawable/ic_check_all.xml new file mode 100644 index 0000000..3300963 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_all.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_check_black_24dp.xml b/app/src/main/res/drawable/ic_check_black_24dp.xml new file mode 100644 index 0000000..76c5eb5 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_black_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 0000000..bb50cad --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_check_circle_outlined.xml b/app/src/main/res/drawable/ic_check_circle_outlined.xml new file mode 100644 index 0000000..f34aa6f --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle_outlined.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml new file mode 100644 index 0000000..cf6f7c1 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_circular_document.xml b/app/src/main/res/drawable/ic_circular_document.xml new file mode 100644 index 0000000..c9d7259 --- /dev/null +++ b/app/src/main/res/drawable/ic_circular_document.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_circular_group.xml b/app/src/main/res/drawable/ic_circular_group.xml new file mode 100644 index 0000000..96945b2 --- /dev/null +++ b/app/src/main/res/drawable/ic_circular_group.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_circular_group_mentions.xml b/app/src/main/res/drawable/ic_circular_group_mentions.xml new file mode 100644 index 0000000..4b79539 --- /dev/null +++ b/app/src/main/res/drawable/ic_circular_group_mentions.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_circular_link.xml b/app/src/main/res/drawable/ic_circular_link.xml new file mode 100644 index 0000000..c50f82f --- /dev/null +++ b/app/src/main/res/drawable/ic_circular_link.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_circular_location.xml b/app/src/main/res/drawable/ic_circular_location.xml new file mode 100644 index 0000000..c84170b --- /dev/null +++ b/app/src/main/res/drawable/ic_circular_location.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_circular_lock.xml b/app/src/main/res/drawable/ic_circular_lock.xml new file mode 100644 index 0000000..adbce1c --- /dev/null +++ b/app/src/main/res/drawable/ic_circular_lock.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_circular_mail.xml b/app/src/main/res/drawable/ic_circular_mail.xml new file mode 100644 index 0000000..ca78fc3 --- /dev/null +++ b/app/src/main/res/drawable/ic_circular_mail.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_clear_24.xml b/app/src/main/res/drawable/ic_clear_24.xml new file mode 100644 index 0000000..2d97d4d --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_close_search.xml b/app/src/main/res/drawable/ic_close_search.xml new file mode 100644 index 0000000..32170dc --- /dev/null +++ b/app/src/main/res/drawable/ic_close_search.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_comment.xml b/app/src/main/res/drawable/ic_comment.xml new file mode 100644 index 0000000..595b3a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_comment.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_contacts.xml b/app/src/main/res/drawable/ic_contacts.xml new file mode 100644 index 0000000..3761a75 --- /dev/null +++ b/app/src/main/res/drawable/ic_contacts.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_content_copy.xml b/app/src/main/res/drawable/ic_content_copy.xml new file mode 100644 index 0000000..f39c284 --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_crop_16_9.xml b/app/src/main/res/drawable/ic_crop_16_9.xml new file mode 100644 index 0000000..e3d2b74 --- /dev/null +++ b/app/src/main/res/drawable/ic_crop_16_9.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_crop_4_3.xml b/app/src/main/res/drawable/ic_crop_4_3.xml new file mode 100644 index 0000000..2c84543 --- /dev/null +++ b/app/src/main/res/drawable/ic_crop_4_3.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..43706dd --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_delete_black_24dp.xml b/app/src/main/res/drawable/ic_delete_black_24dp.xml new file mode 100644 index 0000000..f9bccc7 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_black_24dp.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_delete_grey600_24dp.xml b/app/src/main/res/drawable/ic_delete_grey600_24dp.xml new file mode 100644 index 0000000..186303f --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_grey600_24dp.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_dots_horizontal.xml b/app/src/main/res/drawable/ic_dots_horizontal.xml new file mode 100644 index 0000000..8291b33 --- /dev/null +++ b/app/src/main/res/drawable/ic_dots_horizontal.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_dots_horizontal_white.xml b/app/src/main/res/drawable/ic_dots_horizontal_white.xml new file mode 100644 index 0000000..6ca3fc6 --- /dev/null +++ b/app/src/main/res/drawable/ic_dots_horizontal_white.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000..a2fbad4 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_edit_24.xml b/app/src/main/res/drawable/ic_edit_24.xml new file mode 100644 index 0000000..a2fbad4 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_edit_note_24.xml b/app/src/main/res/drawable/ic_edit_note_24.xml new file mode 100644 index 0000000..4b7b7cd --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_note_24.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_email.xml b/app/src/main/res/drawable/ic_email.xml new file mode 100644 index 0000000..d0f7b4e --- /dev/null +++ b/app/src/main/res/drawable/ic_email.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_exit_to_app_black_24dp.xml b/app/src/main/res/drawable/ic_exit_to_app_black_24dp.xml new file mode 100644 index 0000000..d2fe7b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_exit_to_app_black_24dp.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_eye.xml b/app/src/main/res/drawable/ic_eye.xml new file mode 100644 index 0000000..674a321 --- /dev/null +++ b/app/src/main/res/drawable/ic_eye.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_eye_off.xml b/app/src/main/res/drawable/ic_eye_off.xml new file mode 100644 index 0000000..e991708 --- /dev/null +++ b/app/src/main/res/drawable/ic_eye_off.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_folder.xml b/app/src/main/res/drawable/ic_folder.xml new file mode 100644 index 0000000..ebfa181 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_folder_multiple_image.xml b/app/src/main/res/drawable/ic_folder_multiple_image.xml new file mode 100644 index 0000000..480a8f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_multiple_image.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_hand_back_left.xml b/app/src/main/res/drawable/ic_hand_back_left.xml new file mode 100644 index 0000000..b44f5b7 --- /dev/null +++ b/app/src/main/res/drawable/ic_hand_back_left.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_high_quality.xml b/app/src/main/res/drawable/ic_high_quality.xml new file mode 100644 index 0000000..4417d5d --- /dev/null +++ b/app/src/main/res/drawable/ic_high_quality.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_info_white_24dp.xml b/app/src/main/res/drawable/ic_info_white_24dp.xml new file mode 100644 index 0000000..f26ad47 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_white_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_insert_emoticon_black_24dp.xml b/app/src/main/res/drawable/ic_insert_emoticon_black_24dp.xml new file mode 100644 index 0000000..c9c7171 --- /dev/null +++ b/app/src/main/res/drawable/ic_insert_emoticon_black_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_down.xml b/app/src/main/res/drawable/ic_keyboard_arrow_down.xml new file mode 100644 index 0000000..b16e004 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_down.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_up.xml b/app/src/main/res/drawable/ic_keyboard_arrow_up.xml new file mode 100644 index 0000000..93ed065 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_up.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_keyboard_double_arrow_down.xml b/app/src/main/res/drawable/ic_keyboard_double_arrow_down.xml new file mode 100644 index 0000000..7b4ddca --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_double_arrow_down.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..7959dac --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..c48f9d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml new file mode 100644 index 0000000..68eab0d --- /dev/null +++ b/app/src/main/res/drawable/ic_link.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_link_black_24px.xml b/app/src/main/res/drawable/ic_link_black_24px.xml new file mode 100644 index 0000000..7d0672e --- /dev/null +++ b/app/src/main/res/drawable/ic_link_black_24px.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_link_grey600_24px.xml b/app/src/main/res/drawable/ic_link_grey600_24px.xml new file mode 100644 index 0000000..777c526 --- /dev/null +++ b/app/src/main/res/drawable/ic_link_grey600_24px.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_list_empty_error.xml b/app/src/main/res/drawable/ic_list_empty_error.xml new file mode 100644 index 0000000..32ae448 --- /dev/null +++ b/app/src/main/res/drawable/ic_list_empty_error.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_lock_grey600_24px.xml b/app/src/main/res/drawable/ic_lock_grey600_24px.xml new file mode 100644 index 0000000..233ad9e --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_grey600_24px.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_lock_open_grey600_24dp.xml b/app/src/main/res/drawable/ic_lock_open_grey600_24dp.xml new file mode 100644 index 0000000..e68d237 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_open_grey600_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_lock_plus_grey600_24dp.xml b/app/src/main/res/drawable/ic_lock_plus_grey600_24dp.xml new file mode 100644 index 0000000..a29a362 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_plus_grey600_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_lock_white_24px.xml b/app/src/main/res/drawable/ic_lock_white_24px.xml new file mode 100644 index 0000000..b77b7d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_white_24px.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_logo.xml b/app/src/main/res/drawable/ic_logo.xml new file mode 100644 index 0000000..4ef7d92 --- /dev/null +++ b/app/src/main/res/drawable/ic_logo.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_low_quality.xml b/app/src/main/res/drawable/ic_low_quality.xml new file mode 100644 index 0000000..48f2b6b --- /dev/null +++ b/app/src/main/res/drawable/ic_low_quality.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_map_marker.xml b/app/src/main/res/drawable/ic_map_marker.xml new file mode 100644 index 0000000..9c7a82d --- /dev/null +++ b/app/src/main/res/drawable/ic_map_marker.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_menu.xml b/app/src/main/res/drawable/ic_menu.xml new file mode 100644 index 0000000..c1ffd54 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mic_grey_600_24dp.xml b/app/src/main/res/drawable/ic_mic_grey_600_24dp.xml new file mode 100644 index 0000000..748cf9f --- /dev/null +++ b/app/src/main/res/drawable/ic_mic_grey_600_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mic_off_white_24px.xml b/app/src/main/res/drawable/ic_mic_off_white_24px.xml new file mode 100644 index 0000000..157e6fb --- /dev/null +++ b/app/src/main/res/drawable/ic_mic_off_white_24px.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mic_white_24px.xml b/app/src/main/res/drawable/ic_mic_white_24px.xml new file mode 100644 index 0000000..29899e3 --- /dev/null +++ b/app/src/main/res/drawable/ic_mic_white_24px.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_application_pdf.xml b/app/src/main/res/drawable/ic_mimetype_application_pdf.xml new file mode 100755 index 0000000..f725656 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_application_pdf.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_audio.xml b/app/src/main/res/drawable/ic_mimetype_audio.xml new file mode 100755 index 0000000..3059244 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_audio.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_file.xml b/app/src/main/res/drawable/ic_mimetype_file.xml new file mode 100755 index 0000000..09c31e5 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_file.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_folder.xml b/app/src/main/res/drawable/ic_mimetype_folder.xml new file mode 100755 index 0000000..0c9a423 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_folder.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_folder_shared.xml b/app/src/main/res/drawable/ic_mimetype_folder_shared.xml new file mode 100755 index 0000000..04e29c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_folder_shared.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_image.xml b/app/src/main/res/drawable/ic_mimetype_image.xml new file mode 100755 index 0000000..317cdf6 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_image.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_link.xml b/app/src/main/res/drawable/ic_mimetype_link.xml new file mode 100755 index 0000000..1b6181e --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_link.xml @@ -0,0 +1,38 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mimetype_location.xml b/app/src/main/res/drawable/ic_mimetype_location.xml new file mode 100755 index 0000000..c941b43 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_location.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mimetype_package_x_generic.xml b/app/src/main/res/drawable/ic_mimetype_package_x_generic.xml new file mode 100755 index 0000000..050231f --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_package_x_generic.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_text.xml b/app/src/main/res/drawable/ic_mimetype_text.xml new file mode 100755 index 0000000..3031b70 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_text.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mimetype_text_calendar.xml b/app/src/main/res/drawable/ic_mimetype_text_calendar.xml new file mode 100755 index 0000000..508bedc --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_text_calendar.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_text_code.xml b/app/src/main/res/drawable/ic_mimetype_text_code.xml new file mode 100755 index 0000000..86c6767 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_text_code.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_text_vcard.xml b/app/src/main/res/drawable/ic_mimetype_text_vcard.xml new file mode 100755 index 0000000..59fd6e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_text_vcard.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mimetype_video.xml b/app/src/main/res/drawable/ic_mimetype_video.xml new file mode 100755 index 0000000..0c04809 --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_video.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_x_office_document.xml b/app/src/main/res/drawable/ic_mimetype_x_office_document.xml new file mode 100755 index 0000000..5a5054b --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_x_office_document.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_x_office_presentation.xml b/app/src/main/res/drawable/ic_mimetype_x_office_presentation.xml new file mode 100755 index 0000000..8c8c0fe --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_x_office_presentation.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mimetype_x_office_spreadsheet.xml b/app/src/main/res/drawable/ic_mimetype_x_office_spreadsheet.xml new file mode 100755 index 0000000..b6e77ee --- /dev/null +++ b/app/src/main/res/drawable/ic_mimetype_x_office_spreadsheet.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_modification_asc.xml b/app/src/main/res/drawable/ic_modification_asc.xml new file mode 100644 index 0000000..f69695d --- /dev/null +++ b/app/src/main/res/drawable/ic_modification_asc.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_modification_desc.xml b/app/src/main/res/drawable/ic_modification_desc.xml new file mode 100644 index 0000000..f49922a --- /dev/null +++ b/app/src/main/res/drawable/ic_modification_desc.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_more_horiz_black_24dp.xml b/app/src/main/res/drawable/ic_more_horiz_black_24dp.xml new file mode 100644 index 0000000..46a0a8a --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horiz_black_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_note_to_self.xml b/app/src/main/res/drawable/ic_note_to_self.xml new file mode 100644 index 0000000..c07d891 --- /dev/null +++ b/app/src/main/res/drawable/ic_note_to_self.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000..4ef7d92 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_password.xml b/app/src/main/res/drawable/ic_password.xml new file mode 100644 index 0000000..1e385c0 --- /dev/null +++ b/app/src/main/res/drawable/ic_password.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_pencil_grey600_24dp.xml b/app/src/main/res/drawable/ic_pencil_grey600_24dp.xml new file mode 100644 index 0000000..261a13f --- /dev/null +++ b/app/src/main/res/drawable/ic_pencil_grey600_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_people_group_black_24px.xml b/app/src/main/res/drawable/ic_people_group_black_24px.xml new file mode 100644 index 0000000..3761a75 --- /dev/null +++ b/app/src/main/res/drawable/ic_people_group_black_24px.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_phone.xml b/app/src/main/res/drawable/ic_phone.xml new file mode 100644 index 0000000..3547812 --- /dev/null +++ b/app/src/main/res/drawable/ic_phone.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_phone_small.xml b/app/src/main/res/drawable/ic_phone_small.xml new file mode 100644 index 0000000..527de28 --- /dev/null +++ b/app/src/main/res/drawable/ic_phone_small.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..0956d2b --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_reply.xml b/app/src/main/res/drawable/ic_reply.xml new file mode 100644 index 0000000..8ea9bde --- /dev/null +++ b/app/src/main/res/drawable/ic_reply.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_room_service_black_24dp.xml b/app/src/main/res/drawable/ic_room_service_black_24dp.xml new file mode 100644 index 0000000..ef69af2 --- /dev/null +++ b/app/src/main/res/drawable/ic_room_service_black_24dp.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_search_grey.xml b/app/src/main/res/drawable/ic_search_grey.xml new file mode 100644 index 0000000..fb7cc80 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_grey.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_search_white_24dp.xml b/app/src/main/res/drawable/ic_search_white_24dp.xml new file mode 100644 index 0000000..d8381ba --- /dev/null +++ b/app/src/main/res/drawable/ic_search_white_24dp.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_security_white_24dp.xml b/app/src/main/res/drawable/ic_security_white_24dp.xml new file mode 100644 index 0000000..9fadedd --- /dev/null +++ b/app/src/main/res/drawable/ic_security_white_24dp.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..070a353 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_share_action.xml b/app/src/main/res/drawable/ic_share_action.xml new file mode 100644 index 0000000..0aa7ff8 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_action.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_share_variant.xml b/app/src/main/res/drawable/ic_share_variant.xml new file mode 100644 index 0000000..4175037 --- /dev/null +++ b/app/src/main/res/drawable/ic_share_variant.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_signal_wifi_off_white_24dp.xml b/app/src/main/res/drawable/ic_signal_wifi_off_white_24dp.xml new file mode 100644 index 0000000..9a83588 --- /dev/null +++ b/app/src/main/res/drawable/ic_signal_wifi_off_white_24dp.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_size_asc.xml b/app/src/main/res/drawable/ic_size_asc.xml new file mode 100644 index 0000000..d757395 --- /dev/null +++ b/app/src/main/res/drawable/ic_size_asc.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_size_desc.xml b/app/src/main/res/drawable/ic_size_desc.xml new file mode 100644 index 0000000..ab83798 --- /dev/null +++ b/app/src/main/res/drawable/ic_size_desc.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_star_black_24dp.xml b/app/src/main/res/drawable/ic_star_black_24dp.xml new file mode 100644 index 0000000..1c0146e --- /dev/null +++ b/app/src/main/res/drawable/ic_star_black_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_star_border_black_24dp.xml b/app/src/main/res/drawable/ic_star_border_black_24dp.xml new file mode 100644 index 0000000..17d1cd9 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_border_black_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_switch_video_white_24px.xml b/app/src/main/res/drawable/ic_switch_video_white_24px.xml new file mode 100644 index 0000000..a518a82 --- /dev/null +++ b/app/src/main/res/drawable/ic_switch_video_white_24px.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_talk.xml b/app/src/main/res/drawable/ic_talk.xml new file mode 100644 index 0000000..1470a2e --- /dev/null +++ b/app/src/main/res/drawable/ic_talk.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_timer_black_24dp.xml b/app/src/main/res/drawable/ic_timer_black_24dp.xml new file mode 100644 index 0000000..2a88d29 --- /dev/null +++ b/app/src/main/res/drawable/ic_timer_black_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_twitter.xml b/app/src/main/res/drawable/ic_twitter.xml new file mode 100644 index 0000000..0b93cd4 --- /dev/null +++ b/app/src/main/res/drawable/ic_twitter.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user.xml b/app/src/main/res/drawable/ic_user.xml new file mode 100644 index 0000000..76d6e85 --- /dev/null +++ b/app/src/main/res/drawable/ic_user.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user_status_away.xml b/app/src/main/res/drawable/ic_user_status_away.xml new file mode 100644 index 0000000..1f7a59a --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_away.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user_status_busy.xml b/app/src/main/res/drawable/ic_user_status_busy.xml new file mode 100644 index 0000000..d472086 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_busy.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_user_status_dnd.xml b/app/src/main/res/drawable/ic_user_status_dnd.xml new file mode 100644 index 0000000..d57be6a --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_dnd.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user_status_invisible.xml b/app/src/main/res/drawable/ic_user_status_invisible.xml new file mode 100644 index 0000000..954e489 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_invisible.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_videocam_grey_600_24dp.xml b/app/src/main/res/drawable/ic_videocam_grey_600_24dp.xml new file mode 100644 index 0000000..265b10e --- /dev/null +++ b/app/src/main/res/drawable/ic_videocam_grey_600_24dp.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_videocam_off_white_24px.xml b/app/src/main/res/drawable/ic_videocam_off_white_24px.xml new file mode 100644 index 0000000..7ac59c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_videocam_off_white_24px.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_videocam_white_24px.xml b/app/src/main/res/drawable/ic_videocam_white_24px.xml new file mode 100644 index 0000000..461c5f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_videocam_white_24px.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_volume_mute_white_24dp.xml b/app/src/main/res/drawable/ic_volume_mute_white_24dp.xml new file mode 100644 index 0000000..776e4f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_mute_white_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_volume_up_white_24dp.xml b/app/src/main/res/drawable/ic_volume_up_white_24dp.xml new file mode 100644 index 0000000..158e757 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_up_white_24dp.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_warning_white.xml b/app/src/main/res/drawable/ic_warning_white.xml new file mode 100644 index 0000000..6626134 --- /dev/null +++ b/app/src/main/res/drawable/ic_warning_white.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_web.xml b/app/src/main/res/drawable/ic_web.xml new file mode 100644 index 0000000..f8bb7b2 --- /dev/null +++ b/app/src/main/res/drawable/ic_web.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/icon_circular_phone.xml b/app/src/main/res/drawable/icon_circular_phone.xml new file mode 100644 index 0000000..bbf1e80 --- /dev/null +++ b/app/src/main/res/drawable/icon_circular_phone.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_circular_team.xml b/app/src/main/res/drawable/icon_circular_team.xml new file mode 100644 index 0000000..e57c2a1 --- /dev/null +++ b/app/src/main/res/drawable/icon_circular_team.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/incoming_gradient.xml b/app/src/main/res/drawable/incoming_gradient.xml new file mode 100644 index 0000000..6306eb7 --- /dev/null +++ b/app/src/main/res/drawable/incoming_gradient.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/launch_screen.xml b/app/src/main/res/drawable/launch_screen.xml new file mode 100644 index 0000000..ad38424 --- /dev/null +++ b/app/src/main/res/drawable/launch_screen.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/link_text_background.xml b/app/src/main/res/drawable/link_text_background.xml new file mode 100644 index 0000000..2b225d7 --- /dev/null +++ b/app/src/main/res/drawable/link_text_background.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/link_text_no_preview_background.xml b/app/src/main/res/drawable/link_text_no_preview_background.xml new file mode 100644 index 0000000..55346cd --- /dev/null +++ b/app/src/main/res/drawable/link_text_no_preview_background.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/mention_chip.xml b/app/src/main/res/drawable/mention_chip.xml new file mode 100644 index 0000000..f367524 --- /dev/null +++ b/app/src/main/res/drawable/mention_chip.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/online_status.xml b/app/src/main/res/drawable/online_status.xml new file mode 100644 index 0000000..392a28a --- /dev/null +++ b/app/src/main/res/drawable/online_status.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_archive_24.xml b/app/src/main/res/drawable/outline_archive_24.xml new file mode 100644 index 0000000..b94ba5d --- /dev/null +++ b/app/src/main/res/drawable/outline_archive_24.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/outline_forum_24.xml b/app/src/main/res/drawable/outline_forum_24.xml new file mode 100644 index 0000000..500206a --- /dev/null +++ b/app/src/main/res/drawable/outline_forum_24.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/outline_notifications_active_24.xml b/app/src/main/res/drawable/outline_notifications_active_24.xml new file mode 100644 index 0000000..9f73730 --- /dev/null +++ b/app/src/main/res/drawable/outline_notifications_active_24.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/outline_qr_code_24.xml b/app/src/main/res/drawable/outline_qr_code_24.xml new file mode 100644 index 0000000..6686eb4 --- /dev/null +++ b/app/src/main/res/drawable/outline_qr_code_24.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/src/main/res/drawable/reaction_self_background.xml b/app/src/main/res/drawable/reaction_self_background.xml new file mode 100644 index 0000000..057c39c --- /dev/null +++ b/app/src/main/res/drawable/reaction_self_background.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/reaction_self_bottom_sheet_background.xml b/app/src/main/res/drawable/reaction_self_bottom_sheet_background.xml new file mode 100644 index 0000000..bce1de8 --- /dev/null +++ b/app/src/main/res/drawable/reaction_self_bottom_sheet_background.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/record_start.xml b/app/src/main/res/drawable/record_start.xml new file mode 100644 index 0000000..2b0e5a1 --- /dev/null +++ b/app/src/main/res/drawable/record_start.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/record_starting.xml b/app/src/main/res/drawable/record_starting.xml new file mode 100644 index 0000000..f63c311 --- /dev/null +++ b/app/src/main/res/drawable/record_starting.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/record_stop.xml b/app/src/main/res/drawable/record_stop.xml new file mode 100644 index 0000000..d28fc17 --- /dev/null +++ b/app/src/main/res/drawable/record_stop.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/reply_background.xml b/app/src/main/res/drawable/reply_background.xml new file mode 100644 index 0000000..d750b2e --- /dev/null +++ b/app/src/main/res/drawable/reply_background.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/round_bgnd.xml b/app/src/main/res/drawable/round_bgnd.xml new file mode 100644 index 0000000..4f34c7d --- /dev/null +++ b/app/src/main/res/drawable/round_bgnd.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/shape_grouped_incoming_message.xml b/app/src/main/res/drawable/shape_grouped_incoming_message.xml new file mode 100644 index 0000000..283a1e8 --- /dev/null +++ b/app/src/main/res/drawable/shape_grouped_incoming_message.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_grouped_outcoming_message.xml b/app/src/main/res/drawable/shape_grouped_outcoming_message.xml new file mode 100644 index 0000000..98f212f --- /dev/null +++ b/app/src/main/res/drawable/shape_grouped_outcoming_message.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_incoming_message.xml b/app/src/main/res/drawable/shape_incoming_message.xml new file mode 100644 index 0000000..c12234e --- /dev/null +++ b/app/src/main/res/drawable/shape_incoming_message.xml @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_outcoming_message.xml b/app/src/main/res/drawable/shape_outcoming_message.xml new file mode 100644 index 0000000..66cd302 --- /dev/null +++ b/app/src/main/res/drawable/shape_outcoming_message.xml @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_oval.xml b/app/src/main/res/drawable/shape_oval.xml new file mode 100644 index 0000000..c803bec --- /dev/null +++ b/app/src/main/res/drawable/shape_oval.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/trashbin.xml b/app/src/main/res/drawable/trashbin.xml new file mode 100644 index 0000000..632ac0a --- /dev/null +++ b/app/src/main/res/drawable/trashbin.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/upload.xml b/app/src/main/res/drawable/upload.xml new file mode 100644 index 0000000..5dd0115 --- /dev/null +++ b/app/src/main/res/drawable/upload.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/upload_white.xml b/app/src/main/res/drawable/upload_white.xml new file mode 100644 index 0000000..7c44d1d --- /dev/null +++ b/app/src/main/res/drawable/upload_white.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/voice_message_outgoing_seek_bar_slider.xml b/app/src/main/res/drawable/voice_message_outgoing_seek_bar_slider.xml new file mode 100644 index 0000000..6298684 --- /dev/null +++ b/app/src/main/res/drawable/voice_message_outgoing_seek_bar_slider.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/layout-land/activity_profile.xml b/app/src/main/res/layout-land/activity_profile.xml new file mode 100644 index 0000000..f66f4a4 --- /dev/null +++ b/app/src/main/res/layout-land/activity_profile.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-land/activity_translate.xml b/app/src/main/res/layout-land/activity_translate.xml new file mode 100644 index 0000000..2cf1c51 --- /dev/null +++ b/app/src/main/res/layout-land/activity_translate.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-land/reference_inside_message.xml b/app/src/main/res/layout-land/reference_inside_message.xml new file mode 100644 index 0000000..475f50c --- /dev/null +++ b/app/src/main/res/layout-land/reference_inside_message.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/account_item.xml b/app/src/main/res/layout/account_item.xml new file mode 100644 index 0000000..4c72bd9 --- /dev/null +++ b/app/src/main/res/layout/account_item.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_account_verification.xml b/app/src/main/res/layout/activity_account_verification.xml new file mode 100644 index 0000000..4e64db5 --- /dev/null +++ b/app/src/main/res/layout/activity_account_verification.xml @@ -0,0 +1,46 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml new file mode 100644 index 0000000..c55d38e --- /dev/null +++ b/app/src/main/res/layout/activity_chat.xml @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_conversation_info.xml b/app/src/main/res/layout/activity_conversation_info.xml new file mode 100644 index 0000000..2663a8d --- /dev/null +++ b/app/src/main/res/layout/activity_conversation_info.xml @@ -0,0 +1,659 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_conversation_info_edit.xml b/app/src/main/res/layout/activity_conversation_info_edit.xml new file mode 100644 index 0000000..d24ce19 --- /dev/null +++ b/app/src/main/res/layout/activity_conversation_info_edit.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_conversations.xml b/app/src/main/res/layout/activity_conversations.xml new file mode 100644 index 0000000..ab8e7f6 --- /dev/null +++ b/app/src/main/res/layout/activity_conversations.xml @@ -0,0 +1,341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_full_screen_image.xml b/app/src/main/res/layout/activity_full_screen_image.xml new file mode 100644 index 0000000..688b5e7 --- /dev/null +++ b/app/src/main/res/layout/activity_full_screen_image.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_full_screen_media.xml b/app/src/main/res/layout/activity_full_screen_media.xml new file mode 100644 index 0000000..5481092 --- /dev/null +++ b/app/src/main/res/layout/activity_full_screen_media.xml @@ -0,0 +1,31 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_full_screen_text.xml b/app/src/main/res/layout/activity_full_screen_text.xml new file mode 100644 index 0000000..26bd555 --- /dev/null +++ b/app/src/main/res/layout/activity_full_screen_text.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_geocoding.xml b/app/src/main/res/layout/activity_geocoding.xml new file mode 100644 index 0000000..0e921e5 --- /dev/null +++ b/app/src/main/res/layout/activity_geocoding.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_invitations.xml b/app/src/main/res/layout/activity_invitations.xml new file mode 100644 index 0000000..e86164c --- /dev/null +++ b/app/src/main/res/layout/activity_invitations.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_location.xml b/app/src/main/res/layout/activity_location.xml new file mode 100644 index 0000000..05e8c2e --- /dev/null +++ b/app/src/main/res/layout/activity_location.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_locked.xml b/app/src/main/res/layout/activity_locked.xml new file mode 100644 index 0000000..013c837 --- /dev/null +++ b/app/src/main/res/layout/activity_locked.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..e861481 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_message_search.xml b/app/src/main/res/layout/activity_message_search.xml new file mode 100644 index 0000000..e077575 --- /dev/null +++ b/app/src/main/res/layout/activity_message_search.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_open_conversations.xml b/app/src/main/res/layout/activity_open_conversations.xml new file mode 100644 index 0000000..79822f1 --- /dev/null +++ b/app/src/main/res/layout/activity_open_conversations.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml new file mode 100644 index 0000000..311592d --- /dev/null +++ b/app/src/main/res/layout/activity_profile.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_remote_file_browser.xml b/app/src/main/res/layout/activity_remote_file_browser.xml new file mode 100644 index 0000000..d6cf22f --- /dev/null +++ b/app/src/main/res/layout/activity_remote_file_browser.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_server_selection.xml b/app/src/main/res/layout/activity_server_selection.xml new file mode 100644 index 0000000..f32a0c8 --- /dev/null +++ b/app/src/main/res/layout/activity_server_selection.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..d3bbd1b --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,914 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_shared_items.xml b/app/src/main/res/layout/activity_shared_items.xml new file mode 100644 index 0000000..17f0dec --- /dev/null +++ b/app/src/main/res/layout/activity_shared_items.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_switch_account.xml b/app/src/main/res/layout/activity_switch_account.xml new file mode 100644 index 0000000..45d5d79 --- /dev/null +++ b/app/src/main/res/layout/activity_switch_account.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_take_picture.xml b/app/src/main/res/layout/activity_take_picture.xml new file mode 100644 index 0000000..0bf9c14 --- /dev/null +++ b/app/src/main/res/layout/activity_take_picture.xml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_translate.xml b/app/src/main/res/layout/activity_translate.xml new file mode 100644 index 0000000..e2af1b9 --- /dev/null +++ b/app/src/main/res/layout/activity_translate.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_web_view_login.xml b/app/src/main/res/layout/activity_web_view_login.xml new file mode 100644 index 0000000..c97a319 --- /dev/null +++ b/app/src/main/res/layout/activity_web_view_login.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/ban_item_list.xml b/app/src/main/res/layout/ban_item_list.xml new file mode 100644 index 0000000..e2b3170 --- /dev/null +++ b/app/src/main/res/layout/ban_item_list.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml new file mode 100644 index 0000000..dd67216 --- /dev/null +++ b/app/src/main/res/layout/call_activity.xml @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/call_notification_activity.xml b/app/src/main/res/layout/call_notification_activity.xml new file mode 100644 index 0000000..82afba2 --- /dev/null +++ b/app/src/main/res/layout/call_notification_activity.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/call_started_message.xml b/app/src/main/res/layout/call_started_message.xml new file mode 100644 index 0000000..7dd2f1a --- /dev/null +++ b/app/src/main/res/layout/call_started_message.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/call_states.xml b/app/src/main/res/layout/call_states.xml new file mode 100644 index 0000000..e6ab09e --- /dev/null +++ b/app/src/main/res/layout/call_states.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/create_thread_view.xml b/app/src/main/res/layout/create_thread_view.xml new file mode 100644 index 0000000..704dcc9 --- /dev/null +++ b/app/src/main/res/layout/create_thread_view.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/current_account_item.xml b/app/src/main/res/layout/current_account_item.xml new file mode 100644 index 0000000..86de06d --- /dev/null +++ b/app/src/main/res/layout/current_account_item.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_attachment.xml b/app/src/main/res/layout/dialog_attachment.xml new file mode 100644 index 0000000..0f6895b --- /dev/null +++ b/app/src/main/res/layout/dialog_attachment.xml @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_audio_output.xml b/app/src/main/res/layout/dialog_audio_output.xml new file mode 100644 index 0000000..e5aef96 --- /dev/null +++ b/app/src/main/res/layout/dialog_audio_output.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_ban_participant.xml b/app/src/main/res/layout/dialog_ban_participant.xml new file mode 100644 index 0000000..0e70695 --- /dev/null +++ b/app/src/main/res/layout/dialog_ban_participant.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_choose_account.xml b/app/src/main/res/layout/dialog_choose_account.xml new file mode 100644 index 0000000..224b10c --- /dev/null +++ b/app/src/main/res/layout/dialog_choose_account.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_choose_account_share_to.xml b/app/src/main/res/layout/dialog_choose_account_share_to.xml new file mode 100644 index 0000000..35ec567 --- /dev/null +++ b/app/src/main/res/layout/dialog_choose_account_share_to.xml @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/app/src/main/res/layout/dialog_conversation_operations.xml b/app/src/main/res/layout/dialog_conversation_operations.xml new file mode 100644 index 0000000..108f761 --- /dev/null +++ b/app/src/main/res/layout/dialog_conversation_operations.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_create_conversation.xml b/app/src/main/res/layout/dialog_create_conversation.xml new file mode 100644 index 0000000..52bdbce --- /dev/null +++ b/app/src/main/res/layout/dialog_create_conversation.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_file_attachment_preview.xml b/app/src/main/res/layout/dialog_file_attachment_preview.xml new file mode 100644 index 0000000..ac6ed18 --- /dev/null +++ b/app/src/main/res/layout/dialog_file_attachment_preview.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_filter_conversation.xml b/app/src/main/res/layout/dialog_filter_conversation.xml new file mode 100644 index 0000000..c2e9a02 --- /dev/null +++ b/app/src/main/res/layout/dialog_filter_conversation.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_message_actions.xml b/app/src/main/res/layout/dialog_message_actions.xml new file mode 100644 index 0000000..423bfe6 --- /dev/null +++ b/app/src/main/res/layout/dialog_message_actions.xml @@ -0,0 +1,652 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_message_reactions.xml b/app/src/main/res/layout/dialog_message_reactions.xml new file mode 100644 index 0000000..7a65b46 --- /dev/null +++ b/app/src/main/res/layout/dialog_message_reactions.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_more_call_actions.xml b/app/src/main/res/layout/dialog_more_call_actions.xml new file mode 100644 index 0000000..74df6cf --- /dev/null +++ b/app/src/main/res/layout/dialog_more_call_actions.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_password.xml b/app/src/main/res/layout/dialog_password.xml new file mode 100644 index 0000000..a464715 --- /dev/null +++ b/app/src/main/res/layout/dialog_password.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/layout/dialog_poll_create.xml b/app/src/main/res/layout/dialog_poll_create.xml new file mode 100644 index 0000000..54e3b32 --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_create.xml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_poll_loading.xml b/app/src/main/res/layout/dialog_poll_loading.xml new file mode 100644 index 0000000..23b5637 --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_loading.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/app/src/main/res/layout/dialog_poll_main.xml b/app/src/main/res/layout/dialog_poll_main.xml new file mode 100644 index 0000000..7dcf6ad --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_main.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_poll_results.xml b/app/src/main/res/layout/dialog_poll_results.xml new file mode 100644 index 0000000..00d8082 --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_results.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_poll_vote.xml b/app/src/main/res/layout/dialog_poll_vote.xml new file mode 100644 index 0000000..fe2f634 --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_vote.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_rename_conversation.xml b/app/src/main/res/layout/dialog_rename_conversation.xml new file mode 100644 index 0000000..734ba91 --- /dev/null +++ b/app/src/main/res/layout/dialog_rename_conversation.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_scope.xml b/app/src/main/res/layout/dialog_scope.xml new file mode 100644 index 0000000..c9b3bbe --- /dev/null +++ b/app/src/main/res/layout/dialog_scope.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_set_online_status.xml b/app/src/main/res/layout/dialog_set_online_status.xml new file mode 100644 index 0000000..ea2498a --- /dev/null +++ b/app/src/main/res/layout/dialog_set_online_status.xml @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_set_phone_number.xml b/app/src/main/res/layout/dialog_set_phone_number.xml new file mode 100644 index 0000000..9587107 --- /dev/null +++ b/app/src/main/res/layout/dialog_set_phone_number.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_set_status_message.xml b/app/src/main/res/layout/dialog_set_status_message.xml new file mode 100644 index 0000000..84b3e16 --- /dev/null +++ b/app/src/main/res/layout/dialog_set_status_message.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_temp_message_actions.xml b/app/src/main/res/layout/dialog_temp_message_actions.xml new file mode 100644 index 0000000..98d51ba --- /dev/null +++ b/app/src/main/res/layout/dialog_temp_message_actions.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/edit_message_view.xml b/app/src/main/res/layout/edit_message_view.xml new file mode 100644 index 0000000..263e3fe --- /dev/null +++ b/app/src/main/res/layout/edit_message_view.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/empty_list.xml b/app/src/main/res/layout/empty_list.xml new file mode 100644 index 0000000..9e9441e --- /dev/null +++ b/app/src/main/res/layout/empty_list.xml @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/federated_invitation_hint.xml b/app/src/main/res/layout/federated_invitation_hint.xml new file mode 100644 index 0000000..972ab5c --- /dev/null +++ b/app/src/main/res/layout/federated_invitation_hint.xml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_dialog_ban_list.xml b/app/src/main/res/layout/fragment_dialog_ban_list.xml new file mode 100644 index 0000000..6324cd3 --- /dev/null +++ b/app/src/main/res/layout/fragment_dialog_ban_list.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_message_input.xml b/app/src/main/res/layout/fragment_message_input.xml new file mode 100644 index 0000000..7507959 --- /dev/null +++ b/app/src/main/res/layout/fragment_message_input.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_message_input_voice_recording.xml b/app/src/main/res/layout/fragment_message_input_voice_recording.xml new file mode 100644 index 0000000..d6c1bdc --- /dev/null +++ b/app/src/main/res/layout/fragment_message_input_voice_recording.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/geocoding_item.xml b/app/src/main/res/layout/geocoding_item.xml new file mode 100644 index 0000000..9b3add5 --- /dev/null +++ b/app/src/main/res/layout/geocoding_item.xml @@ -0,0 +1,43 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_incoming_deck_card_message.xml b/app/src/main/res/layout/item_custom_incoming_deck_card_message.xml new file mode 100644 index 0000000..cb09c26 --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_deck_card_message.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_custom_incoming_link_preview_message.xml b/app/src/main/res/layout/item_custom_incoming_link_preview_message.xml new file mode 100644 index 0000000..5d7adb0 --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_link_preview_message.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_incoming_location_message.xml b/app/src/main/res/layout/item_custom_incoming_location_message.xml new file mode 100644 index 0000000..2798d5c --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_location_message.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_incoming_poll_message.xml b/app/src/main/res/layout/item_custom_incoming_poll_message.xml new file mode 100644 index 0000000..a1ca843 --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_poll_message.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_incoming_preview_message.xml b/app/src/main/res/layout/item_custom_incoming_preview_message.xml new file mode 100644 index 0000000..3dc1120 --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_preview_message.xml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_incoming_text_message.xml b/app/src/main/res/layout/item_custom_incoming_text_message.xml new file mode 100644 index 0000000..0533e32 --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_text_message.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_custom_incoming_text_message_shimmer.xml b/app/src/main/res/layout/item_custom_incoming_text_message_shimmer.xml new file mode 100644 index 0000000..fe1629e --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_text_message_shimmer.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_incoming_voice_message.xml b/app/src/main/res/layout/item_custom_incoming_voice_message.xml new file mode 100644 index 0000000..1e2d959 --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_voice_message.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_outcoming_deck_card_message.xml b/app/src/main/res/layout/item_custom_outcoming_deck_card_message.xml new file mode 100644 index 0000000..cdc309e --- /dev/null +++ b/app/src/main/res/layout/item_custom_outcoming_deck_card_message.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_outcoming_link_preview_message.xml b/app/src/main/res/layout/item_custom_outcoming_link_preview_message.xml new file mode 100644 index 0000000..b86fc0f --- /dev/null +++ b/app/src/main/res/layout/item_custom_outcoming_link_preview_message.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_outcoming_location_message.xml b/app/src/main/res/layout/item_custom_outcoming_location_message.xml new file mode 100644 index 0000000..72a97c8 --- /dev/null +++ b/app/src/main/res/layout/item_custom_outcoming_location_message.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_outcoming_poll_message.xml b/app/src/main/res/layout/item_custom_outcoming_poll_message.xml new file mode 100644 index 0000000..81c0015 --- /dev/null +++ b/app/src/main/res/layout/item_custom_outcoming_poll_message.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_outcoming_preview_message.xml b/app/src/main/res/layout/item_custom_outcoming_preview_message.xml new file mode 100644 index 0000000..c6880b8 --- /dev/null +++ b/app/src/main/res/layout/item_custom_outcoming_preview_message.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_outcoming_text_message.xml b/app/src/main/res/layout/item_custom_outcoming_text_message.xml new file mode 100644 index 0000000..61ae98f --- /dev/null +++ b/app/src/main/res/layout/item_custom_outcoming_text_message.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_outcoming_voice_message.xml b/app/src/main/res/layout/item_custom_outcoming_voice_message.xml new file mode 100644 index 0000000..8cea9c5 --- /dev/null +++ b/app/src/main/res/layout/item_custom_outcoming_voice_message.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_event_schedule.xml b/app/src/main/res/layout/item_event_schedule.xml new file mode 100644 index 0000000..2dc61d9 --- /dev/null +++ b/app/src/main/res/layout/item_event_schedule.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_guest_access_settings.xml b/app/src/main/res/layout/item_guest_access_settings.xml new file mode 100644 index 0000000..41d81aa --- /dev/null +++ b/app/src/main/res/layout/item_guest_access_settings.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_message_quote.xml b/app/src/main/res/layout/item_message_quote.xml new file mode 100644 index 0000000..b81460e --- /dev/null +++ b/app/src/main/res/layout/item_message_quote.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_notification_settings.xml b/app/src/main/res/layout/item_notification_settings.xml new file mode 100644 index 0000000..b7bc918 --- /dev/null +++ b/app/src/main/res/layout/item_notification_settings.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_reactions_tab.xml b/app/src/main/res/layout/item_reactions_tab.xml new file mode 100644 index 0000000..3ef06f9 --- /dev/null +++ b/app/src/main/res/layout/item_reactions_tab.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/item_recording_consent.xml b/app/src/main/res/layout/item_recording_consent.xml new file mode 100644 index 0000000..c4334db --- /dev/null +++ b/app/src/main/res/layout/item_recording_consent.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_spacer.xml b/app/src/main/res/layout/item_spacer.xml new file mode 100644 index 0000000..d68a364 --- /dev/null +++ b/app/src/main/res/layout/item_spacer.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_system_message.xml b/app/src/main/res/layout/item_system_message.xml new file mode 100644 index 0000000..8c6900a --- /dev/null +++ b/app/src/main/res/layout/item_system_message.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_thread_title.xml b/app/src/main/res/layout/item_thread_title.xml new file mode 100644 index 0000000..c0dc63b --- /dev/null +++ b/app/src/main/res/layout/item_thread_title.xml @@ -0,0 +1,42 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/item_webinar_info.xml b/app/src/main/res/layout/item_webinar_info.xml new file mode 100644 index 0000000..57af8f2 --- /dev/null +++ b/app/src/main/res/layout/item_webinar_info.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/lobby_view.xml b/app/src/main/res/layout/lobby_view.xml new file mode 100644 index 0000000..c86a670 --- /dev/null +++ b/app/src/main/res/layout/lobby_view.xml @@ -0,0 +1,38 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/menu_item_sheet.xml b/app/src/main/res/layout/menu_item_sheet.xml new file mode 100644 index 0000000..e01771f --- /dev/null +++ b/app/src/main/res/layout/menu_item_sheet.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/no_saved_messages_view.xml b/app/src/main/res/layout/no_saved_messages_view.xml new file mode 100644 index 0000000..da22be8 --- /dev/null +++ b/app/src/main/res/layout/no_saved_messages_view.xml @@ -0,0 +1,37 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/notifications_warning.xml b/app/src/main/res/layout/notifications_warning.xml new file mode 100644 index 0000000..4155ba3 --- /dev/null +++ b/app/src/main/res/layout/notifications_warning.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/out_of_office_view.xml b/app/src/main/res/layout/out_of_office_view.xml new file mode 100644 index 0000000..ed208ee --- /dev/null +++ b/app/src/main/res/layout/out_of_office_view.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/poll_create_options_item.xml b/app/src/main/res/layout/poll_create_options_item.xml new file mode 100644 index 0000000..1861438 --- /dev/null +++ b/app/src/main/res/layout/poll_create_options_item.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/poll_result_header_item.xml b/app/src/main/res/layout/poll_result_header_item.xml new file mode 100644 index 0000000..481bdd9 --- /dev/null +++ b/app/src/main/res/layout/poll_result_header_item.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/poll_result_voter_item.xml b/app/src/main/res/layout/poll_result_voter_item.xml new file mode 100644 index 0000000..27c2e55 --- /dev/null +++ b/app/src/main/res/layout/poll_result_voter_item.xml @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/poll_result_voters_overview_item.xml b/app/src/main/res/layout/poll_result_voters_overview_item.xml new file mode 100644 index 0000000..9042441 --- /dev/null +++ b/app/src/main/res/layout/poll_result_voters_overview_item.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/layout/predefined_status.xml b/app/src/main/res/layout/predefined_status.xml new file mode 100644 index 0000000..7b75d2c --- /dev/null +++ b/app/src/main/res/layout/predefined_status.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/reaction_item.xml b/app/src/main/res/layout/reaction_item.xml new file mode 100644 index 0000000..443c0d4 --- /dev/null +++ b/app/src/main/res/layout/reaction_item.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/reactions_inside_message.xml b/app/src/main/res/layout/reactions_inside_message.xml new file mode 100644 index 0000000..4a07941 --- /dev/null +++ b/app/src/main/res/layout/reactions_inside_message.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/reference_inside_message.xml b/app/src/main/res/layout/reference_inside_message.xml new file mode 100644 index 0000000..1414d9c --- /dev/null +++ b/app/src/main/res/layout/reference_inside_message.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/remainder_to_delete_conversation.xml b/app/src/main/res/layout/remainder_to_delete_conversation.xml new file mode 100644 index 0000000..9c94b40 --- /dev/null +++ b/app/src/main/res/layout/remainder_to_delete_conversation.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/rv_item_browser_file.xml b/app/src/main/res/layout/rv_item_browser_file.xml new file mode 100644 index 0000000..774b5f9 --- /dev/null +++ b/app/src/main/res/layout/rv_item_browser_file.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/rv_item_contact.xml b/app/src/main/res/layout/rv_item_contact.xml new file mode 100644 index 0000000..2a5a2ab --- /dev/null +++ b/app/src/main/res/layout/rv_item_contact.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/rv_item_conversation_info_participant.xml b/app/src/main/res/layout/rv_item_conversation_info_participant.xml new file mode 100644 index 0000000..9452013 --- /dev/null +++ b/app/src/main/res/layout/rv_item_conversation_info_participant.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/rv_item_conversation_with_last_message.xml b/app/src/main/res/layout/rv_item_conversation_with_last_message.xml new file mode 100644 index 0000000..ad83cb9 --- /dev/null +++ b/app/src/main/res/layout/rv_item_conversation_with_last_message.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/rv_item_conversation_with_last_message_shimmer.xml b/app/src/main/res/layout/rv_item_conversation_with_last_message_shimmer.xml new file mode 100644 index 0000000..1029af4 --- /dev/null +++ b/app/src/main/res/layout/rv_item_conversation_with_last_message_shimmer.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/rv_item_invitation.xml b/app/src/main/res/layout/rv_item_invitation.xml new file mode 100644 index 0000000..f3f3746 --- /dev/null +++ b/app/src/main/res/layout/rv_item_invitation.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/rv_item_load_more.xml b/app/src/main/res/layout/rv_item_load_more.xml new file mode 100644 index 0000000..e93cea8 --- /dev/null +++ b/app/src/main/res/layout/rv_item_load_more.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/rv_item_notification_sound.xml b/app/src/main/res/layout/rv_item_notification_sound.xml new file mode 100644 index 0000000..04312d1 --- /dev/null +++ b/app/src/main/res/layout/rv_item_notification_sound.xml @@ -0,0 +1,34 @@ + + + + + + + diff --git a/app/src/main/res/layout/rv_item_open_conversation.xml b/app/src/main/res/layout/rv_item_open_conversation.xml new file mode 100644 index 0000000..31afe2b --- /dev/null +++ b/app/src/main/res/layout/rv_item_open_conversation.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/rv_item_search_message.xml b/app/src/main/res/layout/rv_item_search_message.xml new file mode 100644 index 0000000..1588ede --- /dev/null +++ b/app/src/main/res/layout/rv_item_search_message.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/rv_item_title_header.xml b/app/src/main/res/layout/rv_item_title_header.xml new file mode 100644 index 0000000..be489c2 --- /dev/null +++ b/app/src/main/res/layout/rv_item_title_header.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/search_layout.xml b/app/src/main/res/layout/search_layout.xml new file mode 100644 index 0000000..fa16d57 --- /dev/null +++ b/app/src/main/res/layout/search_layout.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/shared_item_grid.xml b/app/src/main/res/layout/shared_item_grid.xml new file mode 100644 index 0000000..31cff88 --- /dev/null +++ b/app/src/main/res/layout/shared_item_grid.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/shared_item_list.xml b/app/src/main/res/layout/shared_item_list.xml new file mode 100644 index 0000000..7260192 --- /dev/null +++ b/app/src/main/res/layout/shared_item_list.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/sorting_order_fragment.xml b/app/src/main/res/layout/sorting_order_fragment.xml new file mode 100644 index 0000000..5fadefb --- /dev/null +++ b/app/src/main/res/layout/sorting_order_fragment.xml @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/user_info_details_table_item.xml b/app/src/main/res/layout/user_info_details_table_item.xml new file mode 100644 index 0000000..bf3ab1d --- /dev/null +++ b/app/src/main/res/layout/user_info_details_table_item.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/user_info_details_table_item_shimmer.xml b/app/src/main/res/layout/user_info_details_table_item_shimmer.xml new file mode 100644 index 0000000..68489cd --- /dev/null +++ b/app/src/main/res/layout/user_info_details_table_item_shimmer.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/view_message_input.xml b/app/src/main/res/layout/view_message_input.xml new file mode 100644 index 0000000..6bca893 --- /dev/null +++ b/app/src/main/res/layout/view_message_input.xml @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/chat_call_menu.xml b/app/src/main/res/menu/chat_call_menu.xml new file mode 100644 index 0000000..2e7c5b1 --- /dev/null +++ b/app/src/main/res/menu/chat_call_menu.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/menu/chat_send_menu.xml b/app/src/main/res/menu/chat_send_menu.xml new file mode 100644 index 0000000..9aeb7a9 --- /dev/null +++ b/app/src/main/res/menu/chat_send_menu.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/menu/menu_conversation.xml b/app/src/main/res/menu/menu_conversation.xml new file mode 100644 index 0000000..3e62215 --- /dev/null +++ b/app/src/main/res/menu/menu_conversation.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_conversation_info.xml b/app/src/main/res/menu/menu_conversation_info.xml new file mode 100644 index 0000000..41676fb --- /dev/null +++ b/app/src/main/res/menu/menu_conversation_info.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/menu/menu_conversation_info_edit.xml b/app/src/main/res/menu/menu_conversation_info_edit.xml new file mode 100644 index 0000000..c655fa6 --- /dev/null +++ b/app/src/main/res/menu/menu_conversation_info_edit.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/menu/menu_conversation_plus_filter.xml b/app/src/main/res/menu/menu_conversation_plus_filter.xml new file mode 100644 index 0000000..4bc6c11 --- /dev/null +++ b/app/src/main/res/menu/menu_conversation_plus_filter.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_geocoding.xml b/app/src/main/res/menu/menu_geocoding.xml new file mode 100644 index 0000000..fb8426c --- /dev/null +++ b/app/src/main/res/menu/menu_geocoding.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/app/src/main/res/menu/menu_locationpicker.xml b/app/src/main/res/menu/menu_locationpicker.xml new file mode 100644 index 0000000..9d759c1 --- /dev/null +++ b/app/src/main/res/menu/menu_locationpicker.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/app/src/main/res/menu/menu_preview.xml b/app/src/main/res/menu/menu_preview.xml new file mode 100644 index 0000000..2c1b57a --- /dev/null +++ b/app/src/main/res/menu/menu_preview.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/menu/menu_profile.xml b/app/src/main/res/menu/menu_profile.xml new file mode 100644 index 0000000..3a4fa63 --- /dev/null +++ b/app/src/main/res/menu/menu_profile.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/menu/menu_search.xml b/app/src/main/res/menu/menu_search.xml new file mode 100644 index 0000000..970712d --- /dev/null +++ b/app/src/main/res/menu/menu_search.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/menu/menu_share_files.xml b/app/src/main/res/menu/menu_share_files.xml new file mode 100644 index 0000000..f696841 --- /dev/null +++ b/app/src/main/res/menu/menu_share_files.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..3751cee --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9d2b0ff83cb5b9d09e3cd9a8ed89aedab17d32ec GIT binary patch literal 4803 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 S?rkRk0000u<&@!Bs3LWBPE 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 IM6N<$g7a&=cmMzZ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..0d30ab720b06115ba0cda67b676a2941231917e0 GIT binary patch literal 14973 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 Hu0mjfn=_g# literal 0 HcmV?d00001 diff --git a/app/src/main/res/raw/librem_by_feandesign_call.ogg b/app/src/main/res/raw/librem_by_feandesign_call.ogg new file mode 100644 index 0000000000000000000000000000000000000000..2d1c7ee9189d19f11fae0c502a210f87a3e00fbb GIT binary patch literal 213909 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

>_k$e^q{7>r-~P*gUHX%oZoW9MEGLn!4>`ni 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 F|)se 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/raw/next_voice_message_doodle.ogg b/app/src/main/res/raw/next_voice_message_doodle.ogg new file mode 100644 index 0000000000000000000000000000000000000000..797871076c673174d234cf5376d15f85cdbcd551 GIT binary patch literal 10580 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 AkN^Mx literal 0 HcmV?d00001 diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000..b4aa47e --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,674 @@ + + + تحرير + إضافة + إضافة إلى الملاحظات + إضافة المحادثة %1$sإلى المُفضّلة + بحث في %s + الحالة غير متصل + أرشفة المحادثة + رد أرشفة محادثة، سيتم إخفاؤها افتراضياً. حدِّد الفلتر \"مُؤرشَفة\" لعرض المحادثات المؤرشفة. الإشارات المباشرة ستظل قابلة ليُشار إليها حتى بعد الأرشفة. + مؤرشفة + مؤرشفة %1$s + البلوتوث + مَخِرج الصوت + الهاتف + مكبر الصوت + سماعة رأس سلكية + تمّ تعيين حالتك تلقائيّاً + صورة الملف الشخصي الرمزية + بالخارج + زر للخلف + حظر + حظر مشارك + قائمة المحظورين + مشغول + التقويم Calendar + خيارات الاتصال المتقدمة + المكالمة ما زالت مستمرة منذ حوالي الساعة. + اتصال بدون إشعار + تمّ منح إذن الكاميرا. رجاءً، إختر الكامير مجدّداً. + إلغاء تسجيل الدخول + اختر صورة الملف الشخصي الرمزية من السحابة + إمسح رسالة الحالة + إمسح رسالة الحالة فيما بعدٌ + إغلاق + أيقونة الإغلاق + تم الاتصال + انقطع الاتصال - الرسالة المطلوب إرسالها تمّ تحضيرها للإرسال في قائمة بحسب الأولوية + قَفْل التسجيل للتسجيل المستمر للرسالة الصوتية + المحادثة للقراءة فقط + تعذّر تمييز المحادثة \"للقراءة فقط\" + محادثات + انشاء محادثة + تكوين إشكالية + مخصص + منطقة خطر + %1$s في %2$s + إمسح صورة الملف الشخصي الرمزية + محادثة محذوفة %1$s + الرجاء عدم الإزعاج + لا تمحُ + تعديل + تحرير رسالة + مؤخّراً recednt + مُشفّر + إقطَع المكالمة + إنهاء المكالمة للجميع + حدثت مشكلة أثناء رفع دردشاتك + حدث خطأ عند محاولة رفع الحظر عن مشارك + تعذّر حفظ %1$s + 15 دقيقة + مُجلّد + التحميل جارٍ … + %1$s (%2$d) + 4 ساعات + تعذّر جلب الدعوات المعلقة + (معدّلة) + ملاحظة داخلية + غير مرئي + تعذّرت استعادة اللغات + فشلت الاستعادة + في وقت لاحقٍ اليوم + مغادرة المكالمة + أنت غادرت المحادثة %1$s + تحميل المزيد من النتائج + قفل المحادثة + رمز القفل + خفض اليد + تمّ تمييز المحادثة %1$s كمقرؤة + تمّ تمييز المحادثة %1$s كغير مقرؤة + مَذكورٌ + الأحدث أولاً + الأقدم أولاً + أ - ي + ي - أ + الأعلى حجماً أولاً + الأقل حجماً أولاً + مُسحت الرسالة بواسطتك + تمّ التعديل من قِبَل %1$s + أنقُر لفتح الاستبيان + لا توجد نتائج + أكتب هنا للبحث … + بحث … + الرسائل + عدم إظهار جميع التنبيهات + تم استيراد الحساب المحدد وهو متاح الآن + عن + مستخدِم نشط + أضف حساباً + تمت جدولة الحساب للحذف ، ولا يمكن تغييره + افتح القائمة الرئيسية + إضافة مرفق + إضِف إيموجي emoji + إضافة إلى المحادثة + إضِف مُشاركين + أضف إلى المفضلة + تم! + الرمز: %1$s + فتح قفل %1$s + لتمكين مكبرات صوت البلوتوث، يرجى منح إذن \"الأجهزة المجاورة\". + الردُّ بمكالمة مرئية + الردُّ بمكالمة صوتية فقط + تغيير مَخرِج الصوت + تبديل الكاميرا + إنهاء المكالمة + تبديل لاقط الصوت + فتح وضعية \"صورة في صورة\" PIP + تبديل إلى الفيديو الذاتي + واردة + اسم المحادثة + تنبيهات المكالمة + %1$s رفع يده + إعادة الاتصال … + يرن + %1$s في مكالمة + %1$s بالهاتف + %1$s بالفيديو + لا يوجد استجابة خلال 45 ثانية ، انقر للمحاولة مرة أخرى + %s اتصال + %s اتصال فيديو + %s اتصال صوتي + لتمكين اتصال الفيديو ، يرجى منح إذن \"الكاميرا\". + إلغاء + فشل في جلب الإمكانيات، جارِ إلالغاء + الشرح + هل تثق في شهادة SSL غير المعروفة حتى الآن ، الصادرة من%1$sإلى %2$s ، الصالحة %3$s إلى%4$s؟ + تحقق من الشهادة + إعدادات SSL الخاص بك تمنع الاتصال + تغيير شهادة المصادقة + تعديل الكلمة السرية + إلغاء التحرير + إلغاء التحرير + مسح جميع الرسائل + تمّ مسح جميع الرسائل + هل تريد مسح جميع الرسائل في هذه المحادثة؟ + تغيير شهادة العميل + تعيين شهادة العميل + و + إنسَخ + تمّ النسخ إلى الحافظة + إنشاء + مُعطّل + تجاهل + حدث خطأ ما! + خيارات أخرى + تعيين + تخطى + غير معروف + اختر شهادة المصادقة + الاتصال جارٍ … + تم + وصف المحادثة + بيانات المحادثة + اتصال فيديو + اتصال فيديو + المحادثة غير موجودة + إعدادات المحادثة + إنضم لمحادثةٍ او ابدأ محادثةً جديدةً + رحّب بالأصدقاء و الزملاء! + أنسخ + إنشاء محادثة جديدة + صَوّت، رجاءً + اليوم + أمس + حذف + حذف الكل + احذف المحادثة + إذا قمت بحذف المحادثة، سوف يتم حذفها أيضًا لدى المشاركين الآخرين. + إحْذِفِ الرسالة + تم حذف الرسالة بنجاح، لكن من الممكن أن تم تسريبها لخدمات أخرى + تمّ حذف المستخدِم %1$s + تخفيض رتبة مشرف + تسجيل رسالة صوتية + أرسل رسالة + الحساب الحالي + خادوم + هل تم تثبيت تطبيق إشعارات الخادم؟ + مستخدم + هل حالة المُستخدِم مُمكّنة؟ + إصدار أندرويد + تطبيق + اسم التطبيق + المستخدمون المسجلون + رقم إصدار التطبيق + تمّ تجاهل توفير البطارية، و كل شيء على ما يرُام + تحسين البطارية تمّ تمكينه؛ الأمر الذي قد يتسبب في مشكلات. يجب عليك تعطيل تحسين البطارية! + إعدادات البطارية + الجهاز + فتح قائمة التحقق لاكتشاف الأعطال و إصلاحها + فتح شاشة الفحص + فتح dontkillmyapp.com + آخر أَمَارَات إدخال push token في \"فاير بيس\" firebase تمّ جلبها + آخر أَمَارَات إدخال push token في \"فاير بيس\" firebase تمّ توليدها + لم يتم تعيين أيّ رموز توكن push token. من فضلك، أنشيء تقريراً حول هذا الخطأ. + أَمَارَة إدخال push token في \"فاير بيس\" firebase + خدمات متجر التطبيقات من قوقل Google Play غير متاحة. و الإشعارات غير مدعومة + خدمات متجر التطبيقات من قوقل Google play + خدمات متجر التطبيقات من قوقل Google play متاحة + آخر تسجيل لعملية إدخال push في وكيل الإدخال push proxy + لم يتم التسجيل حتى الآن في وكيل الإرسال Push proxy + آخر تسجيل لعملية إدخال push في الخادم + لم يتم التسجيل حتى الآن في الخادم + معلومات وصفية + توليد تقرير النظام + هل قناة الإشعار بالمكالمات مُمكّنة؟ + هل قناة الإشعار بالرسائل مُمكّنة؟ + إذونات الإشعارات + الهاتف + رقم إصدار خادوم \"المحادثة\" Talk + رقم إصدار الخادم + خارجي + داخلي + وضعية إرسال الإشارات + كلمة المرور غير صحيحة + الخادم حالياً في وضع الصيانة + التطبيق غير مُحدَّث + التطبيق قديم جدًا ولم يعد مدعوماً من قبل هذا الخادوم. يُرجى التحديث. + تحديث + هل تريد إعادة تفويض هذا الحساب أو مسحه؟ + حفظ هذه الوسائط على وحدة التخزين سيسمح لأي تطبيق آخر على الوحدة بالوصول إلى هذه الوسائط. + إستمرار؟ + لا + حفظ على وحدة التخزين؟ + نعم + لايمكن عرض الاسم، جارِ إلالغاء + تعذر تخزين اسم العرض ، جارِ إلالغاء + تحرير + تحرير + تحرير رسالة + تمّ التحرير من قِبَل المدير + جدْوَل + 8 ساعات + 4 أسابيع + معطل + 1 يوم + 1 ساعة + 1 أسبوع + إنهاء صلاحية رسائل الدردشة + يمكن أن تنتهي صلاحية رسائل الدردشة بعد وقت معين. ملاحظة: لن يتم حذف الملفات التي تمّت مشاركتها في الدردشة عند المالك، و لكن لن تستمر مشاركتها مع الآخرين في المحادثة. + فشل جلب إعدادات الإشارات + قبول + رفض + من %1$s في %2$s + لا توجد دعوات معلقة + لديك دعوات معلقة + العودة + إذن الوصول إلى الملف مطلوب + المستخدم يتابع رابط عام + أنت: %1$s + إعادة توجيه + إعادة توجيه لـ… + معرض الصور + ليس لديك خادم حتى الآن ؟\nإضغط هنا للحصول على واحد من أحد المزودين + الحصول على الشفرة المصدرية + المجموعة + ضيف + وصول الضيف + تعذّر تمكين/منع وصول الضيف + تمكين الضيف من مشاركة الروابط العامة للانضمام إلى المحادثة. + السماح للضيوف + أدخِل كلمة المرور + كلمة مرور الضيف + خطأ في تعيين/إيقاف كلمة المرور + تعيين كلمة مرور لتقييد من يمكنه استخدام الرابط العام. + الحماية بكلمة المرور + إعادة إرسال الدّعوَات + تعذّر إرسال الدّعوَات بسبب خطإٍ ما. + تمّ إرسال الدّعوَات مُجدّداً. + مشاركة رابط المحادثة + أدخِل رسالةً … + تحسين البطارية لا يتم تجاهله. يجب تغيير هذا للتأكد من أن الإشعارات تعمل في الخلفية! الرجاء النقر فوق \"موافق OK\" ثم تحديد \"جميع التطبيقات All apps\" -> %1$s -> \"لا تقم بالتحسين Do not optimize\" + تجاهل توفير البطارية + محادثة مهمة + دعوات + الانضمام إلى محادثات جارية + حفظ + تحتاج إلى إنشاء ميسر جديد قبل أن تتمكن من مغادرة المحادثة + %1$s| آخر تعديل: %2$s + غادر المحادثة + مُغادرة المكالمة … + رخصة جنو العمومية العامة ، الإصدار 3 + الرخصة + %sتم الوصول إلى عدد الاحرف المسموح به + ساحة الانتظار + تمّت جدولة الاجتماع في %1$s + هذا الاجتماع سيبدأ قريباً + أنت حاليا في انتظار الاستقبال + موقعك الحالي + إذن الموقع مطلوب + الموقع غير معلوم + مقفل + أنقُر لفكّ القُفْل + غير معيّن + تمييز كمقروء + تمييز كغير مقروء + فشل + تعذّر إرسال الرسالة: + بدون اتصالٍ + إلغاء الردّ + تمّت قراءة الرسالة + الإرسال + تمّ إرسال الرسالة + لتمكين الاتصال الصوتي ، يرجى منح إذن \"الميكروفون\". + فَاتَتكَ مكالمةٌ من %s + مشرِف + محادثة جديدة + الرؤية + إشاراتٌ غير مقروءة + رسائل غير مقروءة + %1$sغير متاحٍ (غير مثبت أو مقيد من قبل المدير) + ضيف + لا + لا توجد محادثات مفتوحة + لا توجد محادثات مفتوحة يمكنك الانضمام إليها. إمّا أنه لا توجد محادثات مفتوحة أو أنك مُنضّمٌ إليها جميعًا بالفعل. + بدون بروكسي + أنت لست مُخوّلاً لتنشيط الصوت! + أنت لست مُخوّلاً بتنشيط الفيديو! + ليس الآن + قناة إشعار %1$s في %2$s + مكالمات + الإخطار بالمكالمات الواردة + رسائل + الإخطار بالرسائل الواردة + الملفات المرفوعة + الإخطار بنتيجة الرفع + إعدادات الإشعار + الإشعارات لم يتم إعدادها بالشكل الصحيح + تم ضبط إذونات الإشعارات و إعدادات البطارية بالشكل الصحيح لتلقي الإشعارات. إذا كانت لديك مشاكل في تلقي الإشعارات بالرغم من ذلك، يرجى التحقق من تمكين قنوات الإشعارات للمكالمات و الرسائل. يمكن العثور على مزيد من المساعدة على DontKillMyApp.com أو في قائمة التحقق لاستكشاف الأخطاء و إصلاحها. إذا لم يكن ذلك ناجعاً، يرجى الانتقال إلى شاشة فحص الأعطال و إرسال تقرير بالحالة. + فحص و حل أعطال الإشعارات + أخبرني دومًا + أخطرني عند الإشارة إليَّ + لا تخبرني مطلقًا + غير متصل حاليًا، من فضلك التأكد من الاتصال بالانترنت + موافق + فتح المحادثة للمستخدمين المسجلين + مفتوح أيضًا لضيوف التطبيق + المالك + المشارِكون + إضِف مشاركين + كلمة المرور + تعيين الأذونات + بعض الأذونات تمّ رفضها + يرجى السماح بالأذونات + فتح الإعدادات + إمنح الأذونات رجاءً من Settings > Permissions + الحساب غير موجود + المحادثة عبر %s + كتم لاقط الصوت microphone + فعّل الميكروفون + الرسائل + الخصوصية + المعلومات الشخصية + ترقية لمشرف + محادثة عمومية + الإشعارات معطلة + اضغط للتحدث + في وضعية تعطل المايكرفون، علق على الزر & لاستخدام اضغط للتحدث + ذكرني لاحقا + إزالتها مِن المفضلة + حذف المجموعة group و الأعضاء + حذف مشارك + حذف كلمة المرور + إزالة الفريق و الأعضاء + إعادة تسمية المحادثة + تغيير تسمية + رد + ردّ على الخاص + حفظ + تمّ الحفط بنجاح + 30 ثانية + 5 دقائق + 1 دقيقة + 10 دقائق + فوري + 600 + 60 + 30 + 300 + البحث + مَحوُ البحث + اختيار حساب + %1$s ارسل GIF. + لقد قمت بارسال GIF. + %1$sارسل فيديو. + لقد قمت بإرسال فيديو. + %1$sارسل مقطع صوتي. + لقد قمت بإرسال مقطع صوتي. + %1$sارسل صوره. + لقد قمت بإرسال صورة. + %1$s أرسل بطاقة مهام + إفحَص الاتصال بالخادم + الرجاء ترقية %1$s قاعدة البيانات الخاصة بك + فشل في استيراد حساب محدد + الرابط إلى %1$s واجهتك على الويب عند فتحها في المتصفح + استيراد الحساب من تطبيق %1$s + استيراد حساب + استيراد الحسابات من تطبيق %1$s + استيراد حسابات + الرجاء إخراج %1$sمن الصيانة + الرجاء إنهاء %1$s التثبيت الخاص بك + إختبار الاتصال + الخادم ليس فيه تطبيق Talk مدعوم + عنوان الخادم https://… + %1$s يعمل فقط مع %2$s 13 وأكثر + تعيين كلمة مرور جديدة + تعيين كلمة المرور + الإعدادات + تم تحديث حسابك الموجود بالفعل، بدلاً من إضافة حساب جديد + متقدمة + المظهر + المكالمات + رجاءً، تواصل مع المشرف + فتح شاشة فحص الأعطال لاختبار الإعدادات أو تكوين تقرير بالعطل + فحوصات + توجيه لوحة المفاتيح لتعطيل التعلم المخصص (بدون ضمانات) + لوحة المفاتيح في وضع المجهول + بدون صوت + تطبيق التحدث غير مثبت على الخادم الذي تريد المصادقة إليه. + التنبيهات + الإشعارات تمّ رفضها + تمّ منح أدونات الإشعارات + الرسائل + مُطابقة جهات الاتصال بناءً على رقم الهاتف لدمج اختصارات تطبيق \"المحادثة\" Talk في تطبيق \"جهات الاتصال\" على الهاتف + الخطأ رقم 429؛ طلبات كثيرة جداً + بإمكانك تعيين رقم هاتفك حتى يتمكن المستخدمون الآخرون من العثور عليك + ادخل الجوال + رقم الهاتف غير صحيح + تم تعيين رقم الهاتف بنجاح + رقم الهاتف + ارتباط رقم الهاتف + الخصوصية + مضيف البروكسي + كلمة مرور الوكيل proxy + منفذ البروكسي + نوع البروكسي + اسم مستخدم الوكيل proxy + شارك حالتي و أظهر الحالة للآخرين + قراءة الحالة + إعادة تفويض الحساب + حذف + حذف حساب + يرجى تأكيد رغبتك لإزالة الحساب الحالي. + اقفل %1$s بقفل شاشة أندرويد أو باستخدام القياسات الحيوية + انتهت مهلة قفل الشاشة + قفل الشاشة + يمنع اخذ لقطات الشاشة في القائمة الأخيرة وداخل التطبيق + أمان الشاشة + نسخة الخادوم قديمةٌ جداً ولن يتم توفير الدعم لها في الإصدارت القادمة! + نسخة الخادوم قديمةٌ جداً وغير مدعومة من هذه النسخة من تطبيق الأندرويد + خادم غير مدعوم + تطبيق الإشعارات غير مُثبَّت على الخادوم + ليلي + استخدم النظام الافتراضي + المظهر + فاتح + المظهر + مشاركة حالة كتابتي وإظهار حالة كتابة الآخرين + لا تتوفر حالة الكتابة إلا عند استخدام خلفية سريعة الأداء (HPB) + حالة الكتابة + الوكيل بحاجة إلى بيانات تسجيل الدخول + تحذير + يمكن إعادة تفويض الحساب الجاري فقط + شارك جهة اتصال + إذن قراءة جهات الاتصال مطلوب + شارك الموقع الحالي + رابط المشاركة + مشاركة الموقع + شارِك هذا الموقع + إختر حسابًا + عناصر مُشارَكة + بطاقة مهام + صور، و ملفات، ورسائل صوتية ... + لا توجد عناصر مُشارَكة + الموقع + موقع مُشارَك + عندما لا تكون الإشعارات مهيأة بالصورة الصحيحة، أظهِر التحذيرات الاعتيادية. + أظِهر تحذيراً بالإشعارات الاعتيادية + رتِّب حسب + إبدأ الدردشة الجماعية + وقت البدء + تبديل الحساب + الفريق + اختر الملفات + إرسال هذه الملفات إلى %1$s؟ + إرسال هذا الملف إلى %1$s؟ + عذرًا، فشل الرفع + فشلت عملية رفع %1$s + فشل + مشاركة من %1$s + إرفَع من الجهاز + الرَّفْعُ جارٍ … + %1$s إلى %2$s - %3$s\%% + خُذ صورة + خُذ فيديو + مستخدم + تسجيل فيديو من %1$s + تسجيل محادثة Talk من %1$s (%2$s) + أُنقُر مُطوّلاً للتسجيل، ثم أفلِت للإرسال. + الإذن لتسجيل الصوت مطلوب + « إسحب للإلغاء + ويبنار + نعم + الأسبوع القادم + لم يتم حفظ أي رسائل غير متصلة بالإنترنت + لا ارتباط لرقم الهاتف بسبب أذونات ناقصة + \@-إشارة فقط + معطل + التلقائي + 1 ساعة + مُتّصلٌ + حالة الاتصال + فتح المحادثات + فتح في تطبيق الملفات Files + تشغيل/تجميد رسالة صوتية + التحكم في سرعة التشغيل + إضِف خِياراً + تعديل التصويت vote + نهاية الاستبيان poll + هل تريد حقا إنهاء هذا الاستبيان؟ لا يمكن التراجع عن هذا الخيار لاحقاً. + لا يمكنك التصويت مع المزيد من الخيارات في هذا الاستبيان. + إجابات متعددة + حذف الخيار %1$d + الخيار %1$d + الخيارات + إستبيان خاص + سؤال + سؤالك + النتائج + الإعدادات + صوت vote + تمّ إرسال الصوت vote + سبق تعيينها + رفع اليد + الكل + لا يمكن مشاركة الملفات على وحدة التخزين بدون أذونات + المكالمة قيد التسجيل + إلغاء مباشرة التسجيل + فشل التسجيل. الرجاء الاتصال بمسؤول النظام. + بدء التسجيل + هل ترغب حقّاً في إيقاف التسجيل؟ + إيقاف تسجيل المكالمة + إيقاف التسجيل + إيقاف التسجيل جارٍ ... + الإذن بالتسجيل مطلوبٌ لكل المكالمات + قد يتضمن التسجيل صوتك و فيديو من كاميرتك و مشاركة لشاشتك. لذا، فإن موافقتك مطلوبة قبل الانضمام إلى المكالمة. هل توافق؟ + أطلُب الإذن بالتسجيل قبل الانضمام لهذه المحادثة + الإذن بالتسجيل + المكالمة يُمكن أن يتم تسجيلها. + تسجيل + تمّ سحب المحادثة %1$s من قائمة المفضلة + المحادثة %1$s تمّ تغيير تسميتها + إعادة الإرسال + إعادة تعيين الحالة + لا يمكنك الانضمام إلى غرف محادثة أخرى طالما أنت تجري مكالمةً + حفظ + Scan QR Code + قم فقط بالمزامنة مع الخوادم الموثوقة + المتحدة + يراه فقط المستخدِمون على هذا الخادم و الضيوف + محلي + يراه فقط المستخدِمون الذين أمكن مطابقة أرقام هواتفهم عبر تطبيق المحادثة Talk على الهاتف النقّال + خاص + قم بالمزامنة مع الخوادم الموثوقة ودفتر العناوين العالمية والعامة + منشورة + تبديل النطاق + تغيير مستوى الخصوصية لـ%1$s + مرِّر للأسفل + أيقونة البحث + منذ ثوانٍ + مُحدّدة + إرسال بريد + أرسِل إلى + ليس مسموحاً لك مشاركة محتوى هذه الدردشة + إرسالُ إلى… + أرسِل بدون إشعار + تعيين + التقاط صورة الملف الشخصي الرمزية من الكاميرا + تعيين الحالة + تعيين رسالة الحالة + شارِك + الانضمام إلى المحادثة %1$s في %2$s + الصوت + ملف + وسائط + آخَر + استبيان + تسجيل المكالمة + صوت + أظهِر سبب الحظر + عرض المشاركين المحظورين + المفضلة + لا يُمكنُك إجراءُ مكالمة + بدأت مكالمةً + رسالة الحالة + تمّ إرجاع الحالة + تبديل إلى غرفة جانبية + تبديل إلى الغرفة الرئيسية + خُذ صورة + خطأ في أخذ الصورة + لا يمكن أخذ صورة من دون تصريح + أعد أخذ الصورة + إرسال + تبديل الكاميرا + قصقصة الصورة + تصغير حجم الصورة + فتح/قفل الإنارة + 30 دقيقة + هذا الأسبوع + هذه رسالة اختبار + نهاية هذا الأسبوع + الرَّدّ + اليوم + غدا + ترجِم + الترجمة + نسخ النص المترجم + حذف اللغة + إعدادات الجهاز + تعذّرت معرفة اللغة + فشل الترجمة + مِن : + إلى : + و شخص آخر يكتب ... + يكتبون ... + يكتب... + و%1$s آخرون يقومون بالكتابة... + استرجاع المحادثة من الأرشيف + بمجرد إعادة المحادثة من الأرشيف، سوف تظهر مجدداً بشكل تلقائي. + غير مؤرشفة %1$s + رفع الحظر + غير مقروء + رفع صورة رمزية جديدة للملف الشخصي من الجهاز + %1$s خارج المكتب و قد لا يتمكن من الرّد + %1$s خارج المكتب اليوم + البديل: + صورة الملف الشخصي الرمزية للمستخدم + العنوان + الاسم الكامل + البريد الإلكتروني + رقم الهاتف + تويتر + موقع الويب + الحاله + فشل في استرداد المعلومات الشخصية للمستخدم + لم يتم تعيين أي معلومات شخصية + إضافة الاسم والصورة وتفاصيل الاتصال في صفحة ملفك الشخصي. + ماهي حالتك؟ + + أنظُر %d رسالة مشابهة + أنظُر %d رسالة مشابهة + أنظُر %d رسالة مشابهة + أنظُر %d رسائل مشابهة + أنظُر %d رسالة مشابهة + أنظُر %d رسالة مشابهة + + + %d أصوات + %d صوت + %d أصوات + %d أصوات + %d أصوات + %d أصوات + + diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml new file mode 100644 index 0000000..792b9a7 --- /dev/null +++ b/app/src/main/res/values-ast/strings.xml @@ -0,0 +1,359 @@ + + + Editar + Amestar + Amestar a Notes + La conversación «%1$s» metióse en Favoritos + Buscar en: %s + Apaecer desconectáu + Archivóse + Bluetooth + Salida d\'audiu + Teléfonu + Altavoz + Cascos con cable + L\'estáu afitóse automáticamente + Avatar + Ausente + Ocupáu + Calendariu + Concedióse\'l permisu de la cámara. Volvi escoyer la cámara. + Escoyer un avatar de la nube + Borrar el mensaxe del estáu + Borrar el mensaxe l\'estáu dempués de + Zarrar + Conexón afitada + La conversación ye de namás llectura + Conversaciones + Crear una conversación + Personalizar + Zona de peligru + Desaniciar l\'avatar + Desanicióse la conversación «%1$s» + Nun molestar + Nun borrar + Editar + Editar el mensaxe + De recién + Cifróse + Finar la llamada + Hebo un problema al cargar les charres + Nun se pue guardar «%1$s» + 15 minutos + carpeta + Cargando… + %1$s (%2$d) + 4 hores + (editóse) + Invisible + Nun se pudieron recuperar les llingües + La recuperación falló + Colar de la llamada + Colesti de la conversación «%1$s» + Cargar más resultaos + Bloquiar la conversación + Baxar la mano + Primero lo nuevo + Primero lo antiguo + A - Z + Z - A + Primero lo grande + Primero lo pequeño + Desaniciesti\'l mensaxe + Toca p\'abrir la encuesta + Nun hai nengún resultáu de la busca + Buscar… + Mensaxes + Desactivar tolos avisos + Tocante a + Usuariu activu + Amestar una cuenta + Abrir el menú principal + Amestar a la conversación + Amestar participantes + Meter en Favoritos + Desbloquiar «%1$s» + P\'activar los altavoces Bluetooth, concede\'l permisu «Preseos cercanos» + Contestar como videollamada + Camudar la salida d\'audiu + Colgar + Abrir nel mou imaxe sobre imaxe + ENTRANTE + Nome de la conversación + %1$s alzó la mano + SONANDO + %1$s na llamada + P\'activar la comunicación per videu, concede\'l permisu «Cámara». + Encaboxar + Camudar la contraseña + Desaniciar tolos mensaxes + Desaniciáronse tolos mensaxes + ¿De xuru que quies desaniciar tolos mensaxes d\'esta conversación? + Camudar el certificáu del veceru + Configurar el certificáu del veceru + y + Copiar + Copióse nel cartafueyu + Crear + Desactivóse + Escartar + ¡Prodúxose daqué malo! + Más opciones + Afitar + Saltar + Desconocí­u + Conectando… + Fecho + Descripción de la conversación + Información de la conversación + Videllamada + Nun s\'atopó la converación + Xúnite a una conversación o anicia otra + ¡Saluda a amigos y colegues! + Copiar + Crear una encuesta + Güei + Ayeri + Desaniciar + Desanciar too + Desaniciar la conversación + Si desanicies la conversación, tamién la desanicies pa los demás participantes. + Desaniciar el mensaxe + Unviar el mensaxe + Cuenta actual + Srividor + Usuariu + Versión d\'Android + Aplicación + Nome de l\'aplicación + Usuarios rexistraos + Versión de l\'aplicación + Configuración de la batería + Preséu + Abrir dontkillmyapp.com + Servicios de Google Play + Los servicios de Google Play tán disponibles + Xeneración del informe del sistema + Teléfonu + Versión del sirvidor + La contraseña ye inválida + L\'aplicación nun ta anovada + L\'aplicación ye mui antigua y yá nun ye compatible con esti sirvidor. Anuévala. + Anovar + ¿Quies siguir? + Non + + Editar + Editar + Editar el mensaxe + 8 hores + 4 selmanes + Non + 1 día + 1 hora + 1 selmana + Aceptar + Refugar + Nun hai nenguna invitación pendiente + Nun tienes invitaciones pendientes + Atrás + Tu: %1$s + Avanzar + Galería + Consiguir el códigu fonte + Grupu + Convidáu + Introduz una contraseña + Proteición con contraseña + Compartir l\'enllaz de la converación + Converación importante + Invitaciones + Caltener + Colar de la conversación + Colando de la llamada… + Llicencia + Algamóse la llende de %s caráuteres + Sala d\'espera + Tas na sala d\'espera. + Toca pa desbloquiar + Falló + Nun se pue unviar el mensaxe: + Desconectáu + Lleóse\'l mensaxe + Unvióse\'l mensaxe + P\'activar la comunicación per voz, concede\'l permisu «Micrófonu». + Llendador + Conversación nueva + Visibilidá + Menciones ensin lleer + Mensaxes ensin lleer + Convidáu + Non + Nun hai nenguna conversación abierta + ¡Nun tienes permisu p\'activar l\'audiu! + ¡Nun tienes permisu p\'activar el videu! + Agora non + Llamaes + Mensaxes + Xubes + Configuración de los avisos + Normal + Propietariu + Participantes + Amestar participantes + Contraseña + Abrir la configuración + Nun s\'atopó la cuenta + Activar el micrófonu + Mensaxes + Privacidá + Información personal + Conversación pública + Primir pa falar + Quitar de Favoritos + Renomar la conversación + Renomar + Responder + Responder per privao + Guardar + Guardóse correutamente + 30 segundos + 5 minutos + 1 minutu + 10 minutos + 600 + 60 + 30 + 300 + Buscar + %1$s unvió un GIF. + Unviesti un GIF. + %1$s unvió un videu. + Unviesti un videu. + %1$s unvió una imaxe. + Unviesti una imaxe. + Probar la conexón del sirvidor + Anueva la base de datos %1$s + Nun se pue importar la cuenta seleicionada + Fina la instalación de: %1$s + Probando la conexón + Afitar una contraseña nueva + Configuración + Aspeutu + Llamaes + Tecláu d\'incógnitu + Avisos + Mensaxes + El númberu de teléfonu ye inválidu + Númberu de teléfonu + Privacidá + Quitar + Quitar la cuenta + Bloquéu de la pantalla + La versión del sirvidor ye mui antigua y nun va ser compatible cola versión siguiente. + La versión del sirvidor ye mui antigua y nun ye compatible cola d\'esta aplicación + El sirvidor nun ye compatible + Escuridá + estilu + Claridá + Estilu + Alvertencia + Compartir l\'enllaz + Elementos compartíos + Nun hai nengún elementu compartíu + Llocalización + Cambiar de cuenta + Equipu + ¿Quies unviar estos ficheros a %1$s? + ¿Quies unviar esti ficheru a %1$s? + Sentímoslo, la xuba falló + Nun se pue xubir «%1$s» + Fallu + Xubiendo + Facer una semeya + Grabar un videu + Usuariu + Webinariu + + La selmana que vien + Tolos mensaxes + Non + Por defeutu + 1 hora + En llinia + Estáu en llinia + Abrir les conversaciones + Abrir n\'aplicación Ficheros + Editar el votu + Finar la encuesta + Opciones + Encuesta privada + Entruga + Resultaos + Configuración + Afitóse con anterioridá + Alzar la mano + Too + Aniciar la grabación + ¿De xuru que quies dexar de grabar? + Parar la grabación + Dexando de grabar… + Renomóse la conversación «%1$s» + Reafitar l\'estáu + Guardar + Scan QR Code + Namás sincronizar con sirvidores d\'enfotu + Federada + Namás ye visible pa les persones d\'esta instancia y los convidaos + Llocal + Namás ye visible pa les persones que concasen pela integración del númberu de teléfonu pente Talk en móviles + Priváu + Sincroniza con sirvidores d\'enfotu y la llibreta de direiciones global y pública + Espublizóse + hai segundos + Unviar un corréu electrónicu + Nun tienes permisu pa compartir conteníu con esta charra + Unviar ensin avisar + Afitar + Afitar l\'estáu + Afitar el mensaxe del estáu + Compartir + Audiu + Ficheru + Multimedia + Encuesta + Voz + Meter en Favoritos + Nun tienes permisu p\'aniciar llamaes + Mensaxe del estáu + Cambiar a la sala principal + Hebo un error al facer la semeya + Unviar + Cambiar de cámara + Amenorgar el tamañu de la imaxe + 30 minutos + Esta selmana + Esto ye un mensaxe de prueba + Esta fin de selmana + Responder + Güei + Mañana + Traducir + Traducción + Copiar el testu traducíu + Detectar la llingua + Configuración del preséu + Nun se pudo detectar la llingua + De + Pa + Ensin lleer + Direición + Nome completu + Corréu electrónicu + Númberu de teléfonu + Twitter + Sitiu web + Estáu + Videllamada + ¿Cuál ye\'l to estáu? + diff --git a/app/src/main/res/values-b+en+001/strings.xml b/app/src/main/res/values-b+en+001/strings.xml new file mode 100644 index 0000000..5f8ebb7 --- /dev/null +++ b/app/src/main/res/values-b+en+001/strings.xml @@ -0,0 +1,726 @@ + + + Edit + Add + Add to Notes + Added conversation %1$s to favourites + Search in %s + Appear offline + Archive conversation + Once a conversation is archived, it will be hidden by default. Select the filter \"Archived\" to view archived conversations. Direct mentions will still be received. + Archived + Archived %1$s + Audio call + Bluetooth + Audio output + Phone + Speaker + Wired headset + Your status was set automatically + Avatar + Away + Back button + Ban + Ban participant + Bans list + Busy + Calendar + Advanced call options + Note: the call has been going on for an hour already. + Call without notification + Camera permission granted. Please choose camera again. + Cancel Login + Choose avatar from cloud + Clear status message + Clear status message after + Close + Close Icon + Connection established + No connection to server + Connection lost - Sent messages are queued + Lock recording for continuously recording of the voice message + Conversation is archived + Conversation is read only + Failed to set conversation Read-only + Conversations + Create conversation + Create issue + Custom + Danger zone + %1$s in %2$s + Delete avatar + Delete voice recording + Deleted conversation %1$s + Do not disturb + Don\'t clear + Edit + Messages older than 24 hours can not be edited + Edit message + Recent + Encrypted + End call + End call for everyone + There was a problem loading your chats + Error occurred when unbanning participant + Failed to save %1$s + 15 minutes + folder + Loading … + %1$s (%2$d) + Followed threads + 4 hours + Failed to fetch pending invitations + (edited) + Internal note + Invisible + Languages could not be retrieved + Retrieval failed + Later today + Leave call + You left the conversation %1$s + Load more results + Local time: %1$s + Location permission denied + Please enable it in the app settings + Location services disabled + Please enable location services (GPS) to use this feature + Lock conversation + Lock symbol + Lower hand + Marked conversation %1$s as read + Marked conversation %1$s as unread + Mentioned + Newest first + Oldest first + A - Z + Z - A + Biggest first + Smallest first + Message copied + Are you sure you want to delete this message? + Message deleted by you + Edited by %1$s + Tap to open poll + No search results + Start typing to search … + Search … + Messages + Mute all notifications + Selected account is now imported and available + About + Active user + Add account + The account is scheduled for deletion, and cannot be changed + Open main menu + Add attachment + Add emojis + Add to conversation + Add participants + Add to favourites + OK, all done! + Pin: %1$s + Unlock %1$s + To enable Bluetooth speakers please grant \"Nearby devices\" permission. + Answer as video call + Answer as voice call only + Change audio output + Toggle camera + Hang up + Toggle microphone + Open picture-in-picture mode + Switch to self video + INCOMING + Conversation name + Call notifications + %1$s raised the hand + Reconnecting … + RINGING + %1$s in call + %1$s with phone + %1$s with video + No response in 45 seconds, tap to try again + %s call + %s video call + %s voice call + To enable video communication please grant \"Camera\" permission. + Cancel + Failed to fetch capabilities, aborting + Caption + Do you trust the until now unknown SSL certificate, issued by %1$s for %2$s, valid from %3$s to %4$s? + Check out the certificate + Your SSL setup prevented connection + Change authentication certificate + Change Password + Cancel editing + Cancel editing + Delete all messages + All messages were deleted + Do you really want to delete all messages in this conversation? + Change client certificate + Set up client certificate + and + Copy + Copied to clipboard + Create + Disabled + Dismiss + Sorry, something went wrong! + More options + Set + Skip + Unknown + Select authentication certificate + Connecting … + Done + Conversation description + Conversation info + Video call + Voice call + Conversation not found + Conversation settings + Join a conversation or start a new one + Say hi to your friends and colleagues! + Copy + Create a new conversation + Create poll + You: + Today + Yesterday + Delete + Delete all + Delete conversation + If you delete the conversation, it will also be deleted for all other participants. + Delete message + Message deleted successfully, but it might have been leaked to other services + Delete now + User %1$s was removed + Demote from moderator + Record voice message + Send message + Current account + Server + Server notification app installed? + User + User status enabled? + Android version + App + App name + Registered users + App version + Battery optimization is ignored, all fine + Battery optimization is enabled which might cause issues. You should disable battery optimization! + Battery settings + Device + Open troubleshooting checklist + Open diagnosis screen + Open dontkillmyapp.com + Latest firebase push token fetch + Latest firebase push token generation + No firebase push token set. Please create a bug report. + Firebase push token + Google Play services are not available. Notifications are not supported + Google Play services + Google Play services are available + Latest push registration at push proxy + Not yet registered at push proxy + Latest push registration at server + Not yet registered at server + Meta information + Generation of system report + Calls notification channel enabled? + Messages notification channel enabled? + Notification permissions + Phone + Server Talk version + Server version + External + Internal + Signaling Mode + Invalid password + Server is currently in maintenance mode. + App is outdated + The app is too old and no longer supported by this server. Please update. + Update + Do you want to reauthorise or delete this account? + Saving this media to storage will allow any other apps on your device to access it. + Continue? + No + Save to storage? + Yes + Display name couldn\'t be fetched, aborting + Could not store display name, aborting + Edit + Edit + Edit message + Edited by admin + Event conversation menu + Schedule + 8 hours + 4 weeks + Off + 1 day + 1 hour + 1 week + Expire chat messages + Chat messages can be expired after a certain time. Note: Files shared in chat will not be deleted for the owner, but will no longer be shared in the conversation. + Failed to fetch signaling settings + Accept + Reject + from %1$s at %2$s + No pending invitations + You have pending invitations + Back + Permission for file access is required + Filter conversations + User following a public link + You: %1$s + Forward + Forward to … + Gallery + Do you not have a server yet?\nClick here to get one from a provider + Get source code + Group + Guest + Guest access + Cannot enable/disable guest access. + Allow guests to share a public link to join this conversation. + Allow guests + Enter a password + Guest access password + Error during setting/disabling the password. + Set a password to restrict who can use the public link. + Password protection + Resend invitations + Invitations were not send due to an error. + Invitations were sent out again. + Share conversation link + Enter a message … + Battery optimization is not ignored. This should be changed to make sure that notifications work in the background! Please click OK and select \"All apps\" -> %1$s -> Do not optimize + Ignore battery optimization + Important conversation + \"Do not disturb\" user status is ignored for important conversations + Invalid time + Invitations + Join open conversations + Keep + You need to promote a new moderator before you can leave the conversation + %1$s | Last modified: %2$s + Leave conversation + Leaving call … + GNU General Public Licence, Version 3 + Licence + %s characters limit has been hit + Lobby + This meeting is scheduled for %1$s + The meeting will start soon + You are currently waiting in the lobby. + Your current location + location permission is required + Position unknown + Locked + Tap to unlock + Not set + Mark as read + Mark as unread + Conversation marked as important + Conversation unmarked as sensitive + Conversation marked as sensitive + Conversation unmarked as important + Meeting ended + Message added to notes + Failed + Failed to send message: + Offline + Cancel reply + Message read + Sending + Message sent + Microphone is enabled and audio is recording + To enable voice communication please grant \"Microphone\" permission. + You missed a call from %s + Moderator + New conversation + Visibility + Unread mentions + Unread messages + %1$s not available (not installed or restricted by admin) + Guest + No + No open conversations + No open conversations that you can join.\nEither there are no open conversations or you already joined all of them. + No proxy + You are not allowed to activate audio! + You are not allowed to activate video! + Not now + %1$s on %2$s notification channel + Calls + Notify about incoming calls + Messages + Notify about incoming messages + Uploads + Notify about upload progress + Notification settings + Notifications are not set up correctly + Notification permission and battery settings are correctly set up to receive notifications. If you have problems to receive notifications anyway, please check if the notification channels for calls and messages are enabled. Further help can be found at DontKillMyApp.com or at the troubleshooting checklist. If this does not help, please go to diagnosis screen and send a bug report. + Notification troubleshooting + Always notify + Notify when mentioned + Never notify + Currently offline, please check your connectivity + OK + Ongoing meeting + Open conversation to registered users + Also open to guest app users + Owner + Participants + Add participants + Password + Set permissions + Some permissions were denied. + Please allow permissions + Open settings + Please grant permissions at Settings > Permissions + Account not found + Chat via %s + Mute microphone + Enable microphone + Messages + Privacy + Personal Info + Promote to moderator + Public conversation + Push notifications disabled + Sorry something went wrong, error is %1$s + Sorry something went wrong, cannot fetch test push message + Push notification is sent successfully. You should now receive a notification on this device with the title \'Testing push notifications\' + Press-to-transmit + With the microphone disabled, click&hold to use Press-to-transmit + Remind me later + Remove from favourites + Remove group and members + Remove participant + Remove Password + Remove team and members + Rename conversation + Rename + Reply + Reply privately + Room is retained successfully + Save + Saved successfully + 30 seconds + 5 minutes + 1 minute + 10 minutes + Immediate + 600 + 60 + 30 + 300 + Search + Clear search + Select an account + Update message + Send voice recording + Sensitive conversation + Message preview will be disabled in conversation list and notifications + %1$s sent a GIF. + You sent a GIF. + %1$s sent a video. + You sent a video. + %1$s sent an audio. + You sent an audio. + %1$s sent an image. + You sent an image. + %1$s sent a deck card + Test server connection + Please upgrade your %1$s database + Failed to import selected account + The link to your %1$s web interface when you open it in the browser. + Import account from the %1$s app + Import account + Import accounts from the %1$s app + Import accounts + Please bring your %1$s out of maintenance + Please finish your %1$s installation + Testing connection + Server does not have supported Talk app installed + Server address https://… + %1$s only works with %2$s 13 and up + Set new password + Set Password + Settings + Your already existing account was updated, instead of adding a new one + Advanced + Appearance + Calls + Please contact the administrator of + Open diagnosis screen to check settings or create bug report + Diagnosis + Instructs keyboard to disable personalized learning (without guarantees) + Incognito keyboard + No sound + Talk app is not installed on the server you tried to authenticate against + Notifications + Notifications are declined + Notifications are granted + Messages + Match contacts based on phone number to integrate Talk shortcut into system contacts app + Error 429 Too Many Requests + You can set your phone number so other users will be able to find you + Enter phone number + Invalid phone number + Phone number set successfully + Phone number + Phone number integration + Privacy + Proxy host + Proxy password + Proxy port + Proxy type + Proxy username + Share my read-status and show the read-status of others + Read status + Reauthorise account + Remove + Remove account + Please confirm your intent to remove the current account. + Lock %1$s with Android screen lock or supported biometric method + Screen lock inactivity timeout + Screen lock + Prevents screenshots in the recent list and inside the app + Screen security + The server version is very old and will not be supported in the next release! + The server version is too old and not supported by this version of the Android app + Unsupported server + Server notifications app not installed + Set by battery saver + Dark + Use system default + theme + Light + Theme + Share my typing-status and show the typing-status of others + Typing status is only available when using a high performance backend (HPB) + Typing status + Proxy requires credentials + Warning + Only current account can be reauthorised + Share contact + Permission to read contacts is required + Share current location + Share link + Share location + Share this location + Choose account + Shared items + Deck card + Images, files, voice messages … + No shared items + Location + Shared location + When notifications are not set up correctly, show a regular warning + Show regular notification warning + Sort by + Start group chat + Start time + Switch account + Team + Test push notifications + Test results + Today at %1$s + Tomorrow at %1$s + Choose files + Send these files to %1$s? + Send this file to %1$s? + Sorry, upload failed + Failed to upload %1$s + Failure + Share from %1$s + Upload from device + Uploading + %1$s to %2$s - %3$s\%% + Take photo + Take video + User + Video recording from %1$s + Talk recording from %1$s (%2$s) + Hold to record, release to send. + Permission for audio recording is required + « Slide to cancel + Webinar + Yes + Next week + No archived conversations + No offline messages saved + No phone number integration due to missing permissions + All messages + \@-mentions only + Off + Default + Follow conversation settings + 1 hour + Online + Online status + Open conversations + Open in Files app + Open Notes + Go to thread + Play/pause voice message + Playback speed control + Add option + Edit vote + End poll + Do you really want to end this poll? This cannot be undone. + You cannot vote with more options for this poll. + Multiple answers + Delete option %1$d + Option %1$d + Options + Private poll + Question + Your question + Results + Settings + Vote + Vote submitted + Previously set + QR code could not be read + Raise hand + All + Sharing files from storage is not possible without permissions + Recent threads + The call is being recorded + Cancel recording start + The recording failed. Please contact your administrator. + Start recording + Do you really want to stop the recording? + Stop Call recording + Stop recording + Stopping recording … + Recording consent is required for all calls + The recording might include your voice, video from camera, and screen share. Your consent is required before joining the call. Do you consent? + Require recording consent before joining call in this conversation + Recording consent + The call might be recorded. + Recording + Removed conversation %1$s from favourites + Conversation %1$s was renamed + Resend + Reset status + It is not possible to join other rooms while in a call + Save + Scan QR Code + Only synchronize to trusted servers + Federated + Only visible to people on this instance and guests + Local + Only visible to people matched via phone number integration through Talk on mobile + Private + Synchronize to trusted servers and the global and public address book + Published + Scope toggle + Change privacy level of %1$s + Scroll to bottom + Search Icon + seconds ago + Selected + Send email + Send to + You are not allowed to share content to this chat + Send to … + Send without notification + Set + Set avatar from camera + Set status + Set status message + Share + Join conversation %1$s at %2$s + Audio + File + Media + Other + Poll + Call recording + Voice + Show ban reason + Show banned participants + Favourite + You are not allowed to start a call + Create a thread + started a call + Status message + Status Reverted + Switch to breakout room + Switch to main room + Take a photo + Error taking picture + Taking a photo is not possible without permissions + Re-take photo + Send + Switch camera + Crop photo + Reduce image size + Toggle torch + 30 minutes + This week + This is a test message + This weekend + Cancel thread creation + Thread notifications + Reply + Thread title + Today + Tomorrow + Translate + Translation + Copy translated text + Detect language + Device settings + Could not detect language + Translation failed + From + To + and 1 other is typing … + are typing … + is typing … + and %1$s others are typing … + Unarchive conversation + Once a conversation is unarchived, it will be shown by default again. + Unarchived %1$s + Unban + Unread + Upload new avatar from device + %1$s is out of office and might not respond + %1$s is out of office today + Replacement: + User avatar + Address + Full name + Email + Phone number + Twitter + Website + Status + Failed to retrieve personal user information. + No personal info set + Add name, picture and contact details on your profile page. + Video call + What is your status? + + See %d similar message + See %d similar messages + + + This conversation will be automatically deleted for everyone in %1$d day of no activity + This conversation will be automatically deleted for everyone in %1$d days of no activity + + + %d reply + %d replies + + + %d vote + %d votes + + diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml new file mode 100644 index 0000000..faa77d0 --- /dev/null +++ b/app/src/main/res/values-be/strings.xml @@ -0,0 +1,683 @@ + + + Рэдагаваць + Дадаць + Дадаць да Нататак + Размова %1$s дададзена ў абранае + Пошук у %s + Паказваць \"Па-за сеткай\" + Архіваваць размову + Пасля архівавання размова будзе прадвызначана схавана. Выберыце фільтр \"Архіваванае\", каб праглядзець архіваваныя размовы. Прамыя згадкі ўсё роўна будуць атрыманыя. + Архіваванае + Архівавана %1$s + Аўдыявыклік + Bluetooth + Аўдыявыхад + Тэлефон + Дынамік + Правадная гарнітура + Ваш статус будзе зададзены аўтаматычна + Аватар + Адышоў + Кнопка назад + Заблакіраваць + Заблакіраваць удзельніка + Спіс блакіроўвак + Заняты + Каляндар + Дадатковыя опцыі выкліку + Выклік доўжыцца ўжо гадзіну + Выклік без апавяшчэння + Дазвол на камеру атрыманы. Выберыце камеру яшчэ раз. + Скасаваць уваход + Выбраць аватар з воблака + Ачысціць паведамленне статусу + Ачысціць паведамленне статусу пасля + Закрыць + Значок Закрыць + Злучэнне ўсталявана + Няма злучэння з серверам + Злучэнне страчана — адпраўленыя паведамленні пастаўлены ў чаргу + Блакіроўка запісу для бесперапыннага запісу галасавога паведамлення + Размова архівавана + Размова у рэжыме \"толькі для чытання\" + Не атрымалася перавесці размову ў рэжым \"толькі для чытання\" + Размовы + Стварыць размову + Стварыць справаздачу + Уласны + Небяспечная зона + %1$s у %2$s + Выдаліць аватар + Выдаліць галасавы запіс + Выдалена размова %1$s + Не турбаваць + Не ачышчаць + Рэдагаваць + Паведамленні, старэйшыя за 24 гадзіны, нельга рэдагаваць + Рэдагаваць паведамленне + Нядаўнія + Зашыфравана + Завяршыць выклік + Завяршыць выклік для ўсіх + Узнікла праблема з загрузкай вашых чатаў + Адбылася памылка пры разблакіраванні ўдзельніка + Не ўдалося захаваць %1$s + 15 хвілін + папка + Загрузка … + %1$s (%2$d) + 4 гадзіны + Не ўдалося атрымаць запрашэнні, якія чакаюць разгляду + (адрэдагавана) + Унутраная заўвага + Нябачны + Не ўдалося атрымаць мовы + Памылка атрымання + Пазней сёння + Выйсці з выкліку + Вы выйшлі з размовы %1$s + Загрузіць больш вынікаў + Мясцовы час: %1$s + Няма доступу да геалакацыі + Уключыце яго ў наладах праграмы + Службы геалакацыі адключаны + Каб карыстацца гэтай функцыяй, уключыце службы геалакацыі (GPS) + Заблакіраваць размову + Сімвал блакіроўкі + Апусціць руку + Размова %1$s пазначана як прачытаная + Размова %1$s пазначана як непрачытаная + Спачатку новыя + Спачатку старыя + А-Я + Я-А + Спачатку вялікія + Спачатку малыя + Паведамленне скапіявана + Вы ўпэўнены, што хочаце выдаліць гэта паведамленне? + Вы выдалілі паведамленне + Адрэдагаваў(-ла) %1$s + Націсніце, каб адкрыць апытанне + Няма вынікаў пошуку + Пачніце ўводзіць тэкст для пошуку … + Пошук ... + Паведамленні + Адключыць усе апавяшчэнні + Выбраны ўліковы запіс імпартаваны і даступны + Аб праграме + Актыўны карыстальнік + Дадаць уліковы запіс + Запланавана выдаленне ўліковага запісу і таму ён не можа быць зменены + Адкрыць галоўнае меню + Дадаць далучэнне + Дадаць эмодзі + Дадаць да размовы + Дадаць удзельнікаў + У абранае + ОК, усё гатова! + Разблакіраваць %1$s + Каб уключыць Bluetooth дынамікі, дайце доступ да \"Прылад паблізу\". + Прыняць як відэавыклік + Прыняць як галасавы выклік + Змяніць аўдыявыхад + Укл./выкл. камеру + Пакласці слухаўку + Укл./выкл. мікрафон + Пераключыцца на відэа з сабой + УВАХОДНЫ + Назва размовы + Апавяшчэнні аб выкліках + %1$s падняў(-ла) руку + Перазлучэнне ... + ВЫКЛІК + %1$s у выкліку + %1$s з тэлефонам + %1$s з відэа + Няма адказу на працягу 45 секунд, націсніце, каб паспрабаваць яшчэ раз + %s, выклік + %s, відэавыклік + %s, галасавы выклік + Каб уключыць відэасувязь, дайце дазвол на доступ да камеры. + Скасаваць + Не ўдалося атрымаць магчымасці, скасаванне + Подпіс + Ці давяраеце вы дагэтуль невядомаму SSL-сертыфікату, выдадзенаму %1$s для %2$s, які дзейнічае з %3$s да %4$s? + Праверка сертыфіката + Ваша канфігурацыя SSL перашкодзіла злучэнню + Змяніць сертыфікат аўтэнтыфікацыі + Змяніць пароль + Скасаваць рэдагаванне + Скасаваць рэдагаванне + Выдаліць усе паведамленні + Усе паведамленні былі выдалены + Вы сапраўды хочаце выдаліць усе паведамленні ў гэтай размове? + Змяніць сертыфікат кліента + Наладзіць сертыфікат кліента + і + Капіяваць + Скапіявана ў буфер абмену + Стварыць + Адключана + Адхіліць + Нешта пайшло не так! + Больш параметраў + Задаць + Прапусціць + Невядомы + Выберыце сертыфікат аўтэнтыфікацыі + Злучэнне … + Гатова + Апісанне размовы + Інфармацыя пра размову + Відэавыклік + Галасавы выклік + Размова не знойдзена + Налады размовы + Далучайцеся да размовы або пачніце новую + Прывітайцеся з сябрамі і калегамі! + Капіяваць + Стварыць новую размову + Стварыць апытанне + Вы: + Сёння + Учора + Выдаліць + Выдаліць усе + Выдаліць размову + Калі вы выдаліце ​​размову, яна будзе выдалена і для ўсіх астатніх удзельнікаў. + Выдаліць паведамленне + Паведамленне паспяхова выдалена, але яно магло трапіць у іншыя сэрвісы + Выдаліць зараз + Карыстальнік %1$s быў выдалены + Запісаць галасавое паведамленне + Адправіць паведамленне + Бягучы ўліковы запіс + Сервер + Ці ўсталявана на серверы праграма для апавяшчэнняў? + Карыстальнік + Статус карыстальніка ўключаны? + Версія Android + Праграма + Назва праграмы + Зарэгістраваныя карыстальнікі + Версія праграмы + Аптымізацыя батарэі ігнаруецца, усё добра + Аптымізацыя батарэі ўключана, што можа выклікаць праблемы. Вам варта адключыць аптымізацыю батарэі! + Налады батарэі + Прылада + Адкрыць кантрольны спіс вырашэння праблем + Адкрыць экран дыягностыкі + Адкрыць dontkillmyapp.com + Сэрвісы Google Play недаступны. Апавяшчэнні не падтрымліваюцца + Сэрвісы Google Play + Сэрвісы Google Play даступны + Пакуль не зарэгістраваны на серверы + Метаінфармацыя + Стварэнне сістэмнай справаздачы + Канал апавяшчэнняў аб выкліках уключаны? + Дазволы на апавяшчэнні + Тэлефон + Версія сервера Talk + Версія сервера + Знешні + Унутраны + Рэжым сігналізацыі + Няправільны пароль + Сервер знаходзіцца ў рэжыме тэхнічнага абслугоўвання. + Праграма састарэла + Праграма занадта старая і больш не падтрымліваецца гэтым серверам. Абнавіце яе. + Абнавіць + Вы хочаце паўторна аўтарызавацца ці выдаліць гэты ўліковы запіс? + Захаванне гэтага медыяфайла ў сховішчы дазволіць любым іншым праграмам на вашай прыладзе атрымаць да яго доступ. + Працягнуць? + Не + Захаваць у сховішча? + Так + Рэдагаваць + Рэдагаваць + Рэдагаваць паведамленне + Адрэдагавана адміністратарам + Меню размовы пра падзею + Расклад + 8 гадзін + 4 тыдні + Выкл. + 1 дзень + 1 гадзіна + 1 тыдзень + Тэрмін дзеяння паведамленняў + Тэрмін дзеяння паведамленняў у чаце можа скончыцца праз пэўны час. Заўвага: файлы, абагуленыя ў чаце, не будуць выдалены для ўладальніка, але больш не будуць абагулены ў размове. + Прыняць + Адхіліць + ад %1$s у %2$s + Няма запрашэнняў у чаканні + У вас ёсць запрашэнні ў чаканні + Назад + Патрабуецца дазвол на доступ да файлаў + Фільтраваць размовы + Вы: %1$s + Пераслаць + Пераслаць … + Галерэя + Атрымаць зыходны код + Група + Госць + Гасцявы доступ + Немагчыма ўключыць/выключыць гасцявы доступ. + Дазволіць гасцей, каб абагульваць публічную спасылку для далучэння да гэтай размовы. + Дазволіць гасцей + Увядзіце пароль + Пароль гасцявога доступу + Памылка падчас задання/адключэння пароля. + Задайце пароль, каб абмежаваць, хто можа карыстацца публічнай спасылкай. + Абарона паролем + Адправіць запрашэнні паўторна + Запрашэнні не былі адпраўлены з-за памылкі. + Запрашэнні былі адпраўлены зноў. + Абагуліць спасылку на размову + Увядзіце паведамленне … + Аптымізацыя батарэі не ігнаруецца. Гэта трэба змяніць, каб апавяшчэнні працавалі ў фонавым рэжыме! Націсніце \"ОК\" і выберыце \"Усе праграмы\" -> %1$s -> \"Не аптымізаваць\" + Ігнараваць аптымізацыю батарэі + Важная размова + Статус карыстальніка \"Не турбаваць\" ігнаруецца падчас важных размоў + Памылковы час + Запрашэнні + Далучыцца да адкрытых размоў + Пакінуць + %1$s | Апошняе змяненне: %2$s + Выйсці з размовы + Выхад з выкліку … + Агульная публічная ліцэнзія GNU, версія 3 + Ліцэнзія + Дасягнуты ліміт у %s сімвалы(-аў) + Лобі + Гэта сустрэча запланавана на %1$s + Сустрэча неўзабаве пачнецца + Вы зараз чакаеце ў лобі. + Ваша бягучае месцазнаходжанне + патрэбны доступ да геалакацыі + Месцазнаходжанне невядома + Заблакіравана + Націсніце, каб разблакіраваць + Не зададзена + Пазначыць як прачытанае + Пазначыць як непрачытанае + Размова пазначана як важная + З размовы знята пазнака сакрэтнасці + Размова пазначана як сакрэтная + Пазнака важнасці размовы скасавана + Сустрэча завершана + Паведамленне дададзена ў нататкі + Не ўдалося + Не атрымалася адправіць паведамленне: + Па-за сеткай + Скасаваць адказ + Паведамленне прачытана + Адпраўка + Паведамленне адпраўлена + Мікрафон уключаны, і гук запісваецца + Каб уключыць галасавую сувязь, дайце дазвол на доступ да мікрафона. + Вы прапусцілі выклік ад %s + Мадэратар + Новая размова + Бачнасць + Непрачытаныя згадкі + Непрачытаныя паведамленні + %1$s недаступна (не ўсталявана або абмежавана адміністратарам) + Госць + Не + Няма адкрытых размоў + Няма адкрытых размоў, да якіх вы можаце далучыцца.\nАбо адкрытых размоў няма, або вы ўжо далучыліся да ўсіх з іх. + Няма проксі + Вам не дазволена ўключаць гук! + Вам не дазволена ўключаць відэа! + Не цяпер + Выклікі + Апавяшчаць пры ўваходных выкліках + Паведамленні + Апавяшчаць пры ўваходных паведамленнях + Запампоўванні + Апавяшчаць пра ход выканання запампоўвання + Налады апавяшчэнняў + Апавяшчэнні наладжаны няправільна + Вырашэнне праблем з апавяшчэннямі + Заўсёды апавяшчаць + Апавяшчаць пры згадванні + Ніколі не апавяшчаць + Вы па-за сеткай, праверце падключэнне + OK + Бягучая сустрэча + Адкрыць размову для зарэгістраваных карыстальнікаў + Таксама адкрыта для гасцей + Уладальнік + Удзельнікі + Дадаць удзельнікаў + Пароль + Задаць дазволы + Некаторыя дазволы былі адхілены. + Дайце дазволы + Адкрыць налады + Дайце дазволы ў раздзеле \"Налады\" > \"Дазволы\" + Уліковы запіс не знойдзены + Чат праз %s + Выключыць мікрафон + Уключыць мікрафон + Паведамленні + Прыватнасць + Асабістыя звесткі + Публічная размова + Push-апавяшчэнні адключаны + Нешта пайшло не так, памылка: %1$s + Нешта пайшло не так, не ўдалося атрымаць тэставае push-паведамленне + Push-апавяшчэнне паспяхова адпраўлена. Цяпер вы павінны атрымацьна гэтай прыладзе апавяшчэнне з назвай \"Тэставае push-апавяшчэнне\". + Націсні і гавары + Калі мікрафон адключаны, націсніце і ўтрымлівайце, каб выкарыстоўваць функцыю «Націсні і гавары» + Нагадаць пазней + Выдаліць з абранага + Выдаліць групу і ўдзельнікаў + Выдаліць удзельніка + Выдаліць пароль + Выдаліць каманду і членаў + Перайменаваць размову + Перайменаваць + Адказаць + Адказаць прыватна + Пакой паспяхова захаваны + Захаваць + Паспяхова захавана + 30 секунд + 5 хвілін + 1 хвіліна + 10 хвілін + Неадкладна + 600 + 60 + 30 + 300 + Пошук + Ачысціць пошук + Выберыце ўліковы запіс + Абнавіць паведамленне + Адправіць галасавы запіс + Сакрэтная размова + Перадпрагляд паведамленняў будзе адключаны ў спісе размоў і апавяшчэннях + %1$s адправіў(-ла) GIF. + Вы адправілі GIF. + %1$s адправіў(-ла) відэа. + Вы адправілі відэа. + %1$s адправіў(-ла) аўдыя. + Вы адправілі аўдыя. + %1$s адправіў(-ла) відарыс. + Вы адправілі відарыс. + Праверыць злучэнне з серверам + Абнавіце базу даных %1$s + Не ўдалося імпартаваць выбраны ўліковы запіс + Спасылка на вэб-інтэрфейс %1$s пры адкрыцці ў браўзеры. + Імпартаваць уліковы запіс з праграмы %1$s + Імпартаваць уліковы запіс + Імпартаваць уліковыя запісы з праграмы %1$s + Імпартаваць уліковыя запісы + Выведзіце %1$s з рэжыму тэхнічнага абслугоўвання + Звяршыце ўсталяванне %1$s + Праверка злучэння + На серверы не ўсталявана праграма Talk, якая падтрымліваецца + Адрас сервера https://… + %1$s працуе толькі з %2$s версіі 13 і вышэй + Задаць новы пароль + Задаць пароль + Налады + Быў абноўлены ваш існуючы ўліковы запіс замест дадавання новага + Пашыраныя + Знешні выгляд + Выклікі + Звярніцеся да адміністратара + Адкрыйце экран дыягностыкі, каб праверыць налады або стварыць справаздачу пра памылку + Дыягностыка + Дае каманду клавіятуры адключыць персаналізаванае навучанне (без гарантый) + Клавіятура інкогніта + Няма гуку + Праграма Talk не ўсталявана на серверы, на якім вы спрабавалі прайсці аўтэнтыфікацыю + Апавяшчэнні + Апавяшчэнні адхілены + Апавяшчэнні дазволены + Паведамленні + Супастаўленне кантактаў па нумары тэлефона для інтэграцыі хуткага доступу да Talk у сістэмную праграму кантактаў + Памылка 429: занадта шмат запытаў + Вы можаце задаць свой нумар тэлефона, каб іншыя карыстальнікі маглі вас знайсці + Увядзіце нумар тэлефона + Памылковы нумар тэлефона + Нумар тэлефона паспяхова зададзены + Нумар тэлефона + Інтэграцыя нумара тэлефона + Прыватнасць + Хост проксі + Пароль проксі + Порт проксі + Тып проксі + Імя карыстальніка проксі + Выдаліць + Выдаліць уліковы запіс + Час чакання блакіроўкі экрана пры бяздзейнасці + Блакіроўка экрана + Забараняе рабіць здымкі экрана ў спісе нядаўняга і ўнутры праграмы + Бяспека экрана + Версія сервера вельмі старая і не будзе падтрымлівацца ў наступным выпуску! + Версія сервера занадта старая і не падтрымліваецца гэтай версіяй праграмы для Android. + Сервер не падтрымліваецца + Цёмная + Сістэмная + тэма + Светлая + Тэма + Абагуліць мой статус набору тэксту і паказаць статус набору тэксту іншых + Статус набору тэксту даступны толькі пры выкарыстанні высокапрадукцыйнага бэкенда (HPB) + Статус набору тэксту + Проксі-сервер патрабуе ўліковых даных + Папярэджанне + Толькі бягучы ўліковы запіс можа быць перааўтарызаваны + Абагуліць кантакт + Патрабуецца дазвол на чытанне кантактаў + Абагуліць бягучае месцазнаходжанне + Абагуліць спасылку + Абагуліць месцазнаходжанне + Абагуліць гэта месцазнаходжанне + Выберыце ўліковы запіс + Абагуленыя элементы + Відарысы, файлы, галасавыя паведамленні… + Няма абагуленых элементаў + Месцазнаходжанне + Абагуленае месцазнаходжанне + Калі апавяшчэнні настроены няправільна, паказваць звычайнае папярэджанне + Паказваць звычайнае папярэджанне аб апавяшчэннях + Сартаваць па + Пачаць супольны чат + Час пачатку + Змяніць уліковы запіс + Каманда + Тэставае push-апавяшчэнне + Вынікі тэста + Сёння ў %1$s + Заўтра ў %1$s + Выберыце файлы + Адправіць гэтыя файлы карыстальніку %1$s? + Адправіць гэты файл карыстальніку %1$s? + Не ўдалося запампаваць + Не ўдалося запампаваць %1$s + Няўдача + Абагульванне ад %1$s + Запампаваць з прылады + Запампоўванне + %1$s у %2$s - %3$s\%% + Зрабіць фота + Зняць відэа + Карыстальнік + Відэазапіс ад %1$s + Запіс размовы ад %1$s (%2$s) + Утрымлівайце для запісу, адпусціце для адпраўкі. + Патрабуецца дазвол на аўдыязапіс + « Правядзіце пальцам, каб скасаваць + Вэбінар + Так + На наступным тыдні + Няма архіваваных размоў + Усе паведамленні + Толькі згадкі з @ + Выкл. + Прадвызначаныя + 1 гадзіна + У сетцы + Статус у сетцы + Адкрытыя размовы + Адкрыць у Файлах + Адкрыць Нататкі + Перайсці да гутаркі + Прайграць/прыпыніць галасавое паведамленне + Дадаць варыянт + Рэдагаваць голас + Завяршыць апытанне + Вы сапраўды хочаце завяршыць гэта апытанне? Гэта нельга адрабіць. + Вы не можаце прагаласаваць, выбраўшы больш варыянтаў для гэтага апытання. + Некалькі адказаў + Выдаліць варыянт %1$d + Варыянт %1$d + Варыянты + Прыватнае апытанне + Пытанне + Ваша пытанне + Вынікі + Налады + Галасаваць + Голас адпраўлены + Зададзены раней + Не ўдалося прачытаць QR-код + Падняць руку + Усе + Абагульванне файлаў са сховішча немагчыма без дазволаў + Нядаўнія гутаркі + Выклік запісваецца + Не ўдалося зрабіць запіс. Звярніцеся да адміністратара. + Пачаць запіс + Вы сапраўды хочаце спыніць запіс? + Спыніць запіс выкліку + Спыніць запіс + Спыненне запісу … + Для ўсіх выклікаў патрабуецца згода на запіс + Запіс можа ўключаць ваш голас, відэа з камеры і абагульванне экрана. Перад далучэннем да размовы патрэбна ваша згода. Вы даяце згоду? + Патрабаваць згоду на запіс перад далучэннем да выкліку ў гэтай размове + Згода на запіс + Выклік можа быць запісаны. + Запіс + Размова %1$s выдалена з абранага + Размова %1$s перайменавана + Адправіць паўторна + Скінуць статус + Немагчыма далучыцца да іншых пакояў падчас выкліку + Захаваць + Сканіраваць QR-код + Сінхранізацыя толькі з даверанымі серверамі + Федэратыўны + Змяніць узровень прыватнасці %1$s + Прагартаць уніз + Значок пошуку + с таму + Выбрана + Адправіць электронны ліст + Адправіць + Вам забаронена абагульваць змесціва ў гэтым чаце + Адправіць … + Адправіць без апавяшчэння + Задаць + Задаць аватар з камеры + Задаць статус + Задаць паведамленне статусу + Абагуліць + Далучайцеся да размовы %1$s у %2$s + Аўдыя + Файл + Медыя + Іншае + Апытанне + Запіс выкліку + Голас + Паказаць прычыну блакіроўкі + Паказаць заблакіраваных удзельнікаў + Абранае + Вам не дазволена пачынаць выклік + Стварыць гутарку + Паведамленне статусу + Статус вернуты + Пераключыцца на пакой для абмеркавання + Пераключыцца на галоўны пакой + Зрабіць фота + Памылка пры здымку + Немагчыма зрабіць фота без дазволаў + Зрабіць фота паўторна + Адправіць + Пераключыць камеру + Абрэзаць фота + Зменшыць памер відарыса + Уключыць/выключыць ліхтарык + 30 хвілін + На гэтым тыдні + Гэта тэставае паведамленне + У гэты ўік-энд + Скасаваць стварэнне гутаркі + Апавяшчэнні гутаркі + Адказаць + Загаловак гутаркі + Гутарак не знойдзена + Сёння + Заўтра + Перакласці + Пераклад + Скапіяваць перакладзены тэкст + Вызначыць мову + Налады прылады + Не ўдалося вызначыць мову + Не ўдалося перакласці + З + На + і яшчэ 1 пішуць … + пішуць … + піша … + і яшчэ %1$s пішуць … + Разархіваваць размову + Пасля таго, як размова будзе разархівавана, яна зноў будзе прадвызначана паказвацца. + Разархівавана %1$s + Разблакіраваць + Непрачытанае + Запампаваць новы аватар з прылады + %1$s сёння не на працы і можа не адказаць + %1$s сёння не на працы + Замена: + Аватар карыстальніка + Адрас + Поўнае імя + Электронная пошта + Нумар тэлефона + Twitter + Вэб-сайт + Статус + Дадайце імя, аватар і кантактную інфармацыю на старонку вашага профілю. + Відэавыклік + Які ў вас статус? + + Гэтая размова будзе аўтаматычна выдалена для ўсіх праз %1$d дзень бяздзейнасці. + Гэтая размова будзе аўтаматычна выдалена для ўсіх праз %1$d дні бяздзейнасці. + Гэтая размова будзе аўтаматычна выдалена для ўсіх праз %1$d дзён бяздзейнасці. + Гэтая размова будзе аўтаматычна выдалена для ўсіх праз %1$d дзён бяздзейнасці. + + + %d адказ + %d адказы + %d адказаў + %d адказаў + + + %d голас + %d галасы + %d галасоў + %d галасоў + + diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml new file mode 100644 index 0000000..9245474 --- /dev/null +++ b/app/src/main/res/values-bg-rBG/strings.xml @@ -0,0 +1,514 @@ + + + Редактиране + Добавяне + Търсене в %s + Архивирано + Bluetooth /Блутут/ + Аудио изход + Телефон + Високоговорител + Слушалки с кабел + Състоянието ви беше зададено автоматично + Аватар + Винаги + Зает + Kалендар + Разширени опции за повикване + Обаждане без известие + Дадено е право на камера. Моля, изберете камера отново. + Избор на аватар от облака + Изчисти състоянието + Изчистване на съобщение за състоянието след + Затваряне + Осъществена е връзка + Разговори + Създаване на разговор + Персонализиран + Зона на опасност + Изтриване на аватар + Не безпокойте + Да не се изчиства + Променяне + Редактиране на съобщение + Скорошни + Криптиран + Прекратяване на разговора за всички + Имаше проблем със зареждането на вашите чатове + Неуспешно записване %1$s + 15 минути + папка + Зареждане … + %1$s (%2$d) + 4 чàса + Невидим + Напускане на обаждането + Зареждане на още резултати + Заключване на разговор + Символ за заключване + Сваляне на ръка + Първо най-новият + Първо най-старият + A - Z + Z - A + Първо най-големият + Първо най-малкият + Съобщение, изтрито от вас + Докосване, за отворяне на анкета + Няма резултати от търсенето + Започнете да пишете, за търсене … + Търсене … + Съобщения + Избраният профил вече е импортиран и наличен + Относно + Активен потребител + Добавяне на регистрация + Профилът е планиран за изтриване и не може да бъде променен + Отворяне на главното меню + Добавяне на прикачен файл + Добавяне на емотикони + Добави към разговор + Добавяне на участници + Добави към любимите + Добре, всичко е готово! + Pin: %1$s + Отключване %1$s + Отговор с видео обаждане + Отговор само с гласово повикване + Промяна на аудиоизхода + Превключване на камера + Затваряне + Превключване на микрофона + Отваряне в режим картина в картината + Превключване към самозснемане на видео + ВХОДЯЩИ + Име на разговор + Известия за обаждане + %1$s вдигна ръка + Повторно свързване … + ПОЗВЪНЯВАНЕ + %1$sв разговор + %1$sс телефон + %1$sс видео + При липса на отговор до 45 секунди, докоснете, за да опитате отново + %sобаждане + %sвидео обаждане + %sгласово обаждане + Отказ + Прекъсване, извличането на възможности не беше успешно + Вярвате ли на неизвестен досега SSL сертификат, издаден от %1$s за %2$s, валиден от%3$s до %4$s? + Провери сертификата + Вашата настройка на SSL, предотврати връзката + Променете сертификата за удостоверяване + Промяна на парола + Отказ на редактирането + Отказ на редактирането + Изтриване на всички съобщения + Всички съобщения бяха изтрити + Сигурни ли сте, че искате да изтриете всички съобщения в този разговор? + Промяна на сертификата на клиент + Задаване/настройки на сертификат на клиент + и + Копие + Копирано в клипборда + Създаване + Изключено + Отхвърляне + Съжалявам нещо се обърка! + Още опции + Настройка + Пропусни + Неизвестен + Изберете сертификат за удостоверяване + Свързване … + Готово + Информация за разговор + Видео разговор + Гласово обаждане + Разговорът не е намерен + Настройки за разговор + Присъединяване към разговор или започване на нов + Поздравете приятелите и колегите си! + Копиране + Създаване на анкета + Днес + Вчера + Изтриване + Изтриване на всички + Изтриване на разговора + Ако изтриете разговора, той ще бъде изтрит и за всички останали участници. + Изтриване на съобщението + Съобщението е изтрито успешно, но може да е изтекло към други услуги + Понижаване от модератор + Запис на гласово съобщение + Изпрати съобщение + Текущ профил + Сървър + Версия на Android + Приложение + Име на приложението + Регистрирани потребители + Настройки на батерията + Устройство + Телефон + Външен + Вътрешна + Невалидна парола + Обновяване + Искате ли да разрешите повторно този профил или да го изтриете? + Не + Да + Показваното име не може да бъде извлечено, прекъсване + Името за визуализация не може да бъде записано, прекратяване + Редактиране + Редактиране + Редактиране на съобщение + 8 часа + 4 седмици + Изключен + 1 ден + 1 час + 1 седмица + Изтичащи съобщения в чата + Съобщенията в чата могат да изтекат след определено време. Забележка: Файловете, споделени в чата, няма да бъдат изтрити за собственика, но повече няма да бъдат споделяни в разговора. + Извличането на настройките за сигнализиране не беше успешно + Приемам + Отхвърляне + Назад + Нужно е право за достъп до файл + Потребител, който следва публична връзка + Вие: %1$s + Препращане + Препращане към … + Галерия + Все още нямате сървър?\nНатиснете тук, за да вземете от доставчик + Получаване на изходния код + Група + Гост + Достъп за гост + Не може да се активира/деактивира достъп за гост. + Разрешаване на гостите да споделят публична връзка, за да се присъединят към този разговор. + Позволяване на гости + Въвеждане на парола + Парола за достъп на гост + Грешка при задаване/деактивиране на паролата. + Задайте парола, за да ограничите кой може да използва публичната връзка. + Защита с парола + Повторно изпращане на покани + Поканите не бяха изпратени поради грешка. + Поканите бяха изпратени отново. + Споделяне на връзка за разговор + Въвеждане на съобщение … + Важен разговор + Запази + Трябва да повишите нов модератор, преди да можете да напуснете разговора + %1$s Последна промяна: %2$s + Напускане на разговор + Напускане на обаждането … + GNU Генерален Публичен Лиценз, версия 3 + Лиценз + Достигнато %s ограничение на знаците + Лоби + Тази среща е насрочена за %1$s + Срещата ще започне скоро + В момента изчаквате в лобито. + Вашето текущо местоположение + Нужно е право за местоположение + Неизвестна позиция + Заключено + Докоснете, за да отключите + Не е зададено + Маркирай като прочетено + Маркирай като непрочетена + Неуспешно + Неуспешно изпращане на съобщение: + Отказ на отговор + Съобщението е прочетено + Съобщението е изпратено + Пропуснахте обаждане от %s + Модератор + Нов разговор + Видимост + Непрочетени споменавания + Непрочетени съобщения. + %1$s не е наличен (не е инсталиран или е ограничен от администратора) + Гост + Не + Без прокси + Не ви е позволено да активирате аудио! + Не ви е позволено да активирате видео! + Не сега + %1$s на %2$s канал за известие + Обаждания + Уведомяване за входящи повиквания + Съобщения + Уведомяване за входящи съобщения + Качвания + Уведомяване за напредъка на качването + Настройки на известие + Винаги уведомявайте + Уведомяване при споменаване + Никога не уведомявайте + В момента офлайн, моля, проверете връзката си + ОК + Отваряне на разговор за регистрирани потребители + Отворен е и за потребители на приложение за гости + Създател + Участници + Добавяне на участници + Парола + Отваряне на настройките + Няма намерен профил + Чат чрез %s + Изключване на микрофона + Активиране на микрофона + Съобщения + Защита на лични данни + Лични данни + Повишаване до модератор + Push/насочените/ известия са деактивирани + Натисни и говори + Когато микрофонът е деактивиран, кликнете & и задръжте, за да използвате „Натисни и говори“ + Напомни ми по-късно + Премахни от любимите + Премахване на групата и на членовете + Премахване на участник + Преименуване на разговор + Преименуване + Отговори + Отговаряне на лично + Запиши + Успешно запазено + 30 секунди + 5 минути + 1 минута + 10 минути + 600 + 60 + 30 + 300 + Търси + Изчисти търсенето + Избор на профил + %1$s изпратихте GIF. + Изпратихте GIF. + %1$s изпратихте видео. + Изпратихте видео. + %1$s изпратихте аудио. + Изпратихте аудио. + %1$s изпратихте изображение. + Изпратихте изображение. + Тестване на връзката със сървъра + Моля надстройте вашата %1$s база данни + Импортирането на избрания профил не беше успешно + Връзката към вашия %1$s уеб интерфейс, когато го отворите в браузъра. + Импортиране на профил от %1$s приложението + Внеси профил + Импортиране на профили от%1$s приложението + Импортиране на профили + Моля изведете вашата %1$s от поддръжка + Моля довършете вашата %1$s инсталация + Проверка на връзката + Сървърът не поддържа инсталираното приложение Talk/разговор/ + Адрес на сървъра https://… + %1$s работи само с %2$s 13 и нагоре + Настройки + Вашият, вече съществуващ профил е актуализиран, вместо да добавите нов + Допълнителни + Изглед + Обаждания + Инструктира клавиатурата да деактивира персонализираното обучение (без гаранции) + Инкогнито клавиатура + Без звук + Приложението Talk /разговор/ не е инсталирано на сървъра, в който сте се опитали да се удостоверите + Известия + Съобщения + Съпоставяне на контакти въз основа на телефонен номер, за интегриране на пряк път Talk в приложението за системни контакти + Можете да зададете телефонния си номер, така че други потребители да могат да ви намерят + Въвеждане на телефонен номер + Невалиден телефонен номер + Телефонният номер е зададен успешно + Телефонен номер + Интегриране на телефонен номер + Защита на лични данни + Хост + Прокси парола + Порт + Тип прокси + Име на потребител за прокси сървър + Споделяне на моето състояние на четене и показване на състоянието на четене и на другите + Състояние на четене + Повторно оторизиране на профил + Премахване + Изтриване + Моля, потвърдете намерението си за премахване на текущия профил. + Заключване %1$s с Android прилижението за заключване на екрана или с поддържан биометричен метод + Време за изчакване на неактивност преди заключване на екрана + Блокиране на екрана + Предотвратява екранни снимки в списък скорошни и вътре в приложението + Защита на екрана + Версията на сървъра е много стара и няма да се поддържа в следващата издаена версия! + Версията на сървъра е твърде стара и не се поддържа от тази версия на Android приложението + Неподдържан сървър + Тъмна + Използване на система по подразбиране + тема + Светла + Тема + Проксито изисква идентификационни данни + Внимание + Само текущия профил може да бъде упълномощен. + Споделяне на контакт + Нужно е право за четене на контактите + Споделяне на текущо местоположение + Връзка за споделяне + Споделяне на местоположение + Споделяне на това местоположение + Избор на профил + Споделени елементи + Deck карта + Изображения, файлове, гласови съобщения … + Няма споделени елементи + Местоположение + Споделено местоположение + Сортиране по + Начало + Смяна на профил + Избор на файлове + Да се ​​изпратят ли тези файлове до %1$s? + Да се ​​изпрати ли този файл до %1$s? + За съжаление качването беше неуспешно + Неуспешно качване на %1$s + Неуспех + Споделяне от %1$s + Качване от устройство + Качване + %1$s to %2$s - %3$s\%% + Снимане + Заснемане на видео + Потребител + Видеозапис от %1$s + Запис на разговор от %1$s(%2$s) + Задържане за запис, отпускане за изпращане. + Нужно е право за аудио запис + «Плъзнете, за да отмените + Уебинар + Да + Следваща седмица + Няма интеграция на телефонен номер поради липсващи права + Всички съобщения + \@-само споменавания + Изключен + По подразбиране + 1 час + На линия + Състояние + Отворени разговори + Отворяне в приложението Файлове + Възпроизвеждане/пауза на гласово съобщение + Добавяне на опция + Редактиране на гласуване + Край на анкетата + Наистина ли искате да прекратите тази анкета? Това не може да бъде отменено. + Не можете да гласувате с повече опции за тази анкета. + Множество отговори + Опции + Частна анкета + Въпрос + Вашият въпрос + Резултати + Настройки + Глас + Гласуването е изпратено + Предишно зададени + Вдигане на ръка + Всички + Споделянето на файлове от хранилище не е възможно без права + Разговорът се записва + Отмяна на стартирането на записа + Записът е неуспешен. Моля, свържете се с вашия администратор. + Започване на запис + Наистина ли искате да спрете записа? + Спиране на записа на обаждането + Спиране на записването + Спиране на записването ... + Записване + Възстановяване на състоянието + Не е възможно присъединяването към други стаи, докато сте в обаждане. + Записване + Scan QR Code + Синхронизиране само с доверени сървъри + Федериран + Видимо за потребители на тази инстанция на сървъра, както и гости. + Локално + Видим само за хора, открити по телефонен номер, който е зададен в \"Talk\". + Лично + Синхронизиране с доверени сървъри и с глобалната и публичната адресна книга + Публикувано + Превключване на обхват + Промяна на нивото на поверителност на %1$s + Превъртане до долу + преди секунди + Избранo + Изпращане на имейл + Изпращане до + Нямате право да споделяте съдържание в този чат + Изпращане до … + Изпращане без известие + Да се зададе + Задаване на аватар от камерата + Задаване на състояние + Задай състояние + Споделяне + Аудио + Файл + Медия + Други + Анкета + Запис на обаждане + Гласов + Любими + Нямате право да започнете разговор + Съобщение за състояние + Превключване към стая за отделно събрание + Превключване към основна стая + Направете снимка + Грешка при снимане + Правенето на снимка не е възможно без права + Направете отново снимка + Изпращане + Превключване на камера + Изрязване на снимка + Намаляване на размера на изображение + Превключване на фенерче + 30 минути + Тази седмица + Днес + Утре + Превод + Разпознаване на език + Настройки за устройството + Не можа да се установи езика + Неуспешен превод + От + До + Непрочетено + Качеване на нов аватар от устройството + Потребителски аватар + Адрес + Пълно име + Имейл + Телефонен номер + Twitter + Интернет страница + Състояние + Извличането на лична информация на потребител не беше успешно. + Няма зададена лична информация + Добавяне на име, снимка и подробности за контакт към страницата на вашия профил. + Видео разговор + Какво е вашето състояние? + + %dгласувания + %d гласувания + + diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000..244c33e --- /dev/null +++ b/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,650 @@ + + + Edició + Afegeix + Afegeix-ho a les notes + S\'ha afegit la conversa %1$s als preferits + Cerca a %s + Apareixeu fora de línia + Arxivar la conversa + Un cop arxivada una conversa, s\'ocultarà per defecte. Seleccioneu el filtre \"Arxivada\" per a veure les converses arxivades. Es continuaran rebent les mencions directes. + Arxivat + S\'ha arxivat %1$s + Trucada d\'àudio + Bluetooth + Sortida d\'àudio + Telèfon + Altaveu + Auriculars per cable + S\'ha indicat l\'estat automàticament + Avatar + Absent + Botó Enrere + Prohibeix + Prohibeix el participant + Llista de prohibicions + Ocupat + Calendari + Opcions avançades de trucada + La trucada porta una hora en marxa. + Trucada sense notificació + S\'ha concedit el permís de càmera. Torneu a triar la càmera. + Cancel·la l\'inici de sessió + Trieu un avatar del núvol + Esborrar el missatge d\'estat + Esborra el missatge d\'estat després + Tanca + Tanca la icona + S\'ha establert la connexió + No hi ha connexió al servidor + S\'ha perdut la connexió: els missatges enviats s\'han posat a la cua + Bloqueja l\'enregistrament per a l\'enregistrament continu del missatge de veu + La conversa és només de lectura + No s\'ha pogut establir la conversa en només de lectura + Converses + Crea una conversa + Crea un error + Personalitzat + Zona perillosa + %1$s en %2$s + Suprimeix l\'avatar + Conversa suprimida %1$s + No molesteu + No l\'esborris + Editar + Els missatges mes antics de 24 hores no es poden editar + Edició del missatge + Recent + Xifrat + Finalitzar la trucada + Finalitzar la trucada per a tothom + S\'ha produït un problema carregant els xats + S\'ha produït un error en anul·lar la prohibició del participant + No s\'ha pogut desar %1$s + 15 minuts + carpeta + Carregant … + %1$s (%2$d) + 4 hores + No s\'han pogut recuperar les invitacions pendents + (editat) + Nota interna + Invisible + No s\'han pogut recuperar els idiomes + Ha fallat la recuperació + Avui més tard + Abandona la trucada + Heu abandonat la conversa %1$s + Carregar més resultats + Bloqueja la conversa + Símbol de bloqueig + Baixa mà + S\'ha marcat com a llegida la conversa %1$s + S\'ha marcat com a no llegida la conversa %1$s + Mencionat + Més nou primer + Més antic primer + A - Z + Z - A + Més gran primer + Més petit primer + Missatge suprimit per tu + Editat per %1$s + Toqueu per obrir l\'enquesta + No s\'han trobat resultats + Comença a escriure per cercar … + Cerca … + Missatges + Silencieu totes les notificacions + El compte que heu seleccionat s\'ha importat i ja és disponible + Quant a + Usuari actiu + Afegeix un compte + El compte està planificat per ser suprimit i no es pot canviar + Obre el menú principal + Afegeix un adjunt + Afegeix emoji + Afegeix a la conversa + Afegeix participants + Afegeix a preferits + D\'acord, tot fet! + Ancora: %1$s + Desbloca %1$s + Per a habilitar els altaveus Bluetooth, concediu el permís de \"Dispositius propers\". + Respon com a trucada de vídeo + Respon només com a trucada de veu + Canvia la sortida d\'àudio + Commuta la càmera + Penja + Commuta el micròfon + Obre la imatge en mode d\'imatge + Canviar al vídeo propi + ENTRANT + Nom de la conversa + Notificacions de trucada + %1$sha aixecat la mà + S\'està reconnectant… + TRUCANT + %1$s a la trucada + %1$samb telèfon + %1$s amb vídeo + Han passat 45 segons i no hi ha hagut cap resposta, feu un toc i torneu-ho a provar + %strucada + %s trucada de vídeo + %s trucada de veu + Per habilitar la comunicació per vídeo, concediu el permís de \"Càmera\". + Cancel·la + No s\'han pogut obtenir les funcionalitats, s\'està avortant + Subtítol + Us en refieu del certificat SSL desconegut fins ara, emès per %1$s per %2$s, vàlid del %3$s fins el %4$s? + Verifica el certificat + La vostra configuració SSL ha impedit la connexió + Canvia el certificat d’autenticació + Canvia la contrasenya + Cancel·la l\'edició + Cancel·la l\'edició + Suprimeix tots els missatges + S\'han suprimit tots els missatges + Segur que voleu suprimir tots els missatges d\'aquesta conversa? + Canvia el certificat del client + Configureu el certificat del client + i + Còpia + Copiat al porta-retalls + Crea + Inhabilitat + Descarta + Disculpeu, alguna cosa ha anat malament! + Més opcions + Estableix + Omet + Desconegut + Selecciona el certificat d’autenticació + S\'està connectant … + Fet + Descripció de la conversa + Informació de la conversa + Trucada de vídeo + Trucada de veu + No s\'ha trobat la conversa + Paràmetres de la conversa + Uniu-vos a una conversa o inicieu-ne una de nova + Saludeu els vostres amics i col·legues! + Copia + Crea una conversa nova + Crea enquesta + Avui + Ahir + Suprimir + Suprimir-ho tot + Suprimir la conversa + Al suprimir la conversa, també serà suprimit per a la resta de participants. + Suprimeix el missatge + El missatge s\'ha suprimit correctament, però podria haver-se filtrat a altres serveis + S\'ha suprimit l\'usuari %1$s + Deposat des del moderador + Grava un missatge de veu + Envia un missatge + Compte actual + Servidor + S\'ha instal·lat l\'aplicació de notificació del servidor? + Usuari + S\'ha habilitat l\'estat de l\'usuari? + Versió d\'Android + Aplicació + Nom de l\'aplicació + Usuaris registrats + Versió de l\'aplicació + S\'ha ignorat l\'optimització de la bateria, tot bé + L\'optimització de la bateria està habilitada, i això pot causar problemes. Hauríeu de desactivar l\'optimització de la bateria. + Paràmetres de la bateria + Dispositiu + Obre la llista de comprovació de resolució de problemes + Obre la pantalla de diagnòstic + Obre dontkillmyapp.com + Serveis de Google Play no està disponible. No s\'admeten les notificacions. + Serveis de Google Play + Serveis de Google Play està disponible + Últim registre de notificacions al servidor intermediari de notificacions automàtiques + Encara no s\'ha registrat al servidor intermediari de notificacions automàtiques + Últim registre de notificacions automàtiques al servidor + Encara no s\'ha registrat al servidor + Informació de les metadades + Generació de l\'informe del sistema + S\'ha activat el canal de notificació de trucades? + S\'ha habilitat el canal de notificació de missatges? + Permisos de notificacions + Telèfon + Versió de Converses del servidor + Versió del servidor + Extern + Intern + Mode de senyal + Contrasenya no vàlida + El servidor està en mode de manteniment. + L\'aplicació és obsoleta + L\'aplicació és massa antiga i ja no és compatible amb aquest servidor. Actualitzeu-la. + Actualització + Voleu reautoritzar o suprimir aquest compte? + Desar aquest suport per emmagatzemar-lo permetrà que qualsevol altra aplicació del dispositiu hi accedeixi. + Voleu continuar? + No + Voleu desar a l\'emmagatzematge? + + No s’ha pogut obtenir el nom de visualització, s\'està avortant + No s\'ha pogut emmagatzemar el nom de visualització, s\'està avortant + Edició + Edició + Edició del missatge + Editat per l\'administrador + 8 hores + 4 setmanes + Desactivada + 1 dia + 1 hora + 1 setmana + Fes caducar els missatges del xat + Els missatges de xat poden caducar al cap d\'un període de temps determinat. Nota: els fitxers compartits al xat no se suprimiran per al propietari, però ja no es compartiran a la conversa. + No s\'ha pogut obtenir la configuració de senyalització + Accepta + Rebutja + No hi ha cap invitació pendent + Teniu invitacions pendents + Enrere + Cal permís per accedir als fitxers + Usuari que segueix un enllaç públic + Tu: %1$s + Reenvia + Reenvia a… + Galeria + Encara no teniu cap servidor?\nFeu clic aquí per obtenir-ne un d\'un proveïdor + Obté el codi font + Grup + Convidat + Accés de convidats + No es pot habilitar/inhabilitar l\'accés de convidats. + Permet que els visitants comparteixin un enllaç públic per unir-se a aquesta conversa. + Permet visitants + Introduïu una contrasenya + Contrasenya d\'accés de convidat + S\'ha produït un error en establir/inhabilitar la contrasenya. + Establiu una contrasenya per restringir qui pot utilitzar l\'enllaç públic. + Protecció amb contrasenya + Torna a enviar les invitacions + Les invitacions no s\'han enviat a causa d\'un error. + S\'han tornat a enviar les invitacions. + Comparteix l\'enllaç de la conversa + Introduïu un missatge … + L\'optimització de la bateria no s\'ignora. Això hauria de canviar-se per a garantir que les notificacions funcionin en segon pla. Feu clic a D\'acord i seleccioneu \"Totes les aplicacions\" -> %1$s -> No optimitzis + Ignora l\'optimització de la bateria + Conversa important + Invitacions + Uneix-te a converses obertes + Mantén + Heu de promocionar un nou moderador abans de deixar la conversa + %1$s | Darrera modificació: %2$s + Surt de la conversa + S\'està abandonant la trucada… + Llicència Pública General de GNU, Versió 3 + Llicència + S\'ha arribat al límit de %s caràcters + Vestíbul + Aquesta reunió està programada per a %1$s + La reunió començarà aviat + Esteu esperant al vestíbul. + La vostra ubicació actual + es necessita permís d\'ubicació + Posició desconeguda + Bloquejat + Toca per desblocar + No establert + Marca com a llegit + Marca com a sense llegir + Ha fallat + No s\'ha pogut enviar el missatge: + Cancel·la la resposta + Missatge llegit + Missatge enviat + Per a habilitar la comunicació per veu, concediu el permís de \"Micròfon\". + Teniu una trucada perduda de %s + Moderador + Nova conversa + Visibilitat + Mencions no llegides + Missatges sense llegir + %1$s no està disponible (no instal·lat o restringit per l\'administrador) + Convidat + No + No hi ha converses obertes + No hi ha converses obertes a què us pugueu unir.\nO bé no hi ha converses obertes o ja us heu unit a totes. + Sense proxy + No teniu permís per activar l\'àudio + No teniu permís per activar el vídeo + Ara no + %1$s a %2$s canal de notificació + Trucades + Notifica les trucades entrants + Missatges + Notifica els missatges entrants + Pujades + Notifica el progrés de càrrega + Paràmetres de les notificacions + Les notificacions no estan configurades correctament + Els permisos de notificacions i la configuració de la bateria estan configurats correctament per rebre notificacions. Si igualment teniu problemes per rebre notificacions, comproveu si els canals de notificació per a trucades i missatges estan habilitats. Podeu trobar més ajuda a DontKillMyApp.com o a la llista de comprovació de resolució de problemes. Si això no us ajuda, aneu a la pantalla de diagnòstic i envieu un informe d\'error. + Resolució de problemes de notificació + Notifica sempre + Notifica només quan s\'esmenti + No notifiquis mai + Actualment fora de línia, si us plau comproveu la vostra connectivitat + D\'acord + Conversa oberta als usuaris registrats + També obert als usuaris d\'aplicacions convidats + Propietari + Participants + Afegeix participants + Contrasenya + Establiu els permisos + Alguns permisos s\'han denegat. + Permeteu els permisos + Obre la configuració + Concediu els permisos a Configuració > Permisos + No s\'ha trobat el compte + Xateja mitjançant %s + Silencia el micròfon + Activa el micròfon + Missatges + Privadesa + Informació personal + Promou a moderador + Conversa pública + Servei de notificacions desactivat + Prem-i-parla + Amb el micròfon desactivat, feu clic a & i mantingueu-lo pressionat per fer servir el Prem-i-parla + Recorda-m\'ho més tard + Suprimeix de favorits + Suprimeix el grup i els membres + Suprimeix el participant + Suprimeix la contrasenya + Suprimeix l\'equip i els membres + Reanomena la conversa + Anomena + Respon + Respon en privat + Desar + S\'ha desat correctament + 30 segons + 5 minuts + 1 minut + 10 minuts + 600 + 60 + 30 + 300 + Cerca + Neteja la cerca + Selecciona un compte + %1$s ha enviat un GIF. + Heu enviat un GIF. + %1$s ha enviat un vídeo. + Heu enviat un vídeo. + %1$s ha enviat un àudio. + Heu enviat un àudio. + %1$s ha enviat una imatge. + Heu enviat una imatge. + Proveu la connexió amb el servidor + Si us plau, actualitzeu la vostra base de dades %1$s + Ha fallat la importació del compte seleccionat + Enllaç a la interfície web del %1$s quan l\'obriu en el navegador. + importa el compte des de l\'aplicació %1$s + Importa el compte + importa els comptes des de l\'aplicació %1$s + Importa els comptes + Si us plau, porteu el vostre %1$s cap a manteniment + Si us plau, acabeu la vostra instal·lació %1$s + S\'està comprovant la connexió + El servidor no té instal·lada l\'aplicació Talk + Adreça del servidor https://… + %1$s només funciona amb %2$s 13 i endavant + Estableix una contrasenya nova + Defineix la contrasenya + Paràmetres + S\'ha actualitzat el vostre compte ja existent enlloc d’afegir-ne un de nou + Avançat + Aparença + Trucades + Contacteu amb l\'administrador de + Obre la pantalla de diagnòstic per a comprovar la configuració o crear un informe d\'error + Diagnòstic + Indica al teclat que desactivi l\'aprenentatge personalitzat (sense garanties) + Teclat d\'incògnit + Sense so + L\'aplicació Talk no està instal·lada al servidor amb el qual heu intentat autenticar-vos + Notificacions + Les notificacions s\'han rebutjat + S\'han concedit les notificacions + Missatges + Fes coincidir els contactes basats en el número de telèfon per a integrar la drecera de Talk a l\'aplicació de contactes del sistema + Error 429 massa sol·licituds + Podeu establir el vostre número de telèfon perquè altres usuaris puguin trobar-vos + Introduïu el número de telèfon + Número de telèfon no vàlid + El número de telèfon s\'ha definit correctament + Número de telèfon + Integració del número de telèfon + Privadesa + Servidor Proxy + Contrasenya del proxy + Port del Proxy + Tipus de Proxy + Nom d\'usuari del proxy + Comparteix el meu estat de lectura i mostra l\'estat de lectura dels altres + Estat de lectura + Torna a autoritzar el compte + Suprimeix + Suprimeix el compte + Si us plau, confirmeu la vostra intenció de suprimir el compte actual. + Bloca %1$s amb el bloqueig de pantalla d\'Android o mètode biomètric compatible + Temps d\'espera d’inactivitat de bloqueig de pantalla + Bloqueig de pantalla + Impedeix captures de pantalla a la llista recent i dins de l\'aplicació + Seguretat de la pantalla + La versió del servidor és molt antiga i no serà compatible amb la pròxima versió. + La versió del servidor és massa antiga i no és compatible amb versió de l\'aplicació Android + El servidor no és compatible + L\'aplicació de notificacions del servidor no s\'ha instal·lat + Fosc + Fes servir el predeterminat del sistema + tema + Clar + Tema + Comparteix el meu estat d\'escriptura i mostra l\'estat d\'escriptura dels altres + L\'estat d\'escriptura només està disponible quan s\'utilitza un backend d\'alt rendiment (HPB) + Estat d\'escriptura + El proxy necessita credencials + Avís + Només el compte actual pot ser re-autoritzat + Comparteix el contacte + Es necessita permís per llegir els contactes + Comparteix la ubicació actual + Comparteix un enllaç + Comparteix la ubicació + Comparteix aquesta localització + Trieu un compte + Elements compartits + Targeta de Deck + Imatges, fitxers, missatges de veu … + No hi ha elements compartits + Ubicació + Ubicació compartida + Quan les notificacions no estan configurades correctament, mostra un avís regular + Mostra un avís regular de notificacions + Ordena per + Hora d\'inici + Canvia el compte + Equip + Trieu els fitxers + Voleu enviar aquests fitxers a %1$s? + Voleu enviar aquest fitxer a %1$s? + S\'ha produït un error durant la pujada + No s\'ha pogut pujar %1$s + Error + Comparteix des de %1$s + Carrega des del dispositiu + S\'està carregant + %1$s a %2$s - %3$s\%% + Fes una foto + Fes un vídeo + Usuari + S\'està enregistrant un vídeo des de %1$s + Trucada enregistrada des de %1$s(%2$s) + Manteniu premut per gravar, deixeu anar per enviar. + Es necessita permís per gravar àudio + « Llisqueu per cancel·lar + Webinar + + Setmana següent + No s\'han desat els missatges fora de línia + No hi ha integració amb el número de telèfon perquè falten permisos + Apagat + Per defecte + 1 hora + En línia + Estat en línia + Obre les converses + Obre a l\'aplicació Fitxers + Reprodueix/pausa el missatge de veu + Control de velocitat de reproducció + Afegeix una opció + Edita el vot + Finalitza l\'enquesta + Segur que voleu finalitzar aquesta enquesta? Això no es podrà desfer. + No podeu votar amb més opcions per a aquesta enquesta. + Respostes múltiples + Opcions + Enquesta privada + Pregunta + La vostra pregunta + Resultats + Paràmetres + Votar + S\'ha enviat el vot + Definits anteriorment + Aixeca la mà + Totes + No es poden compartir fitxers des de l\'emmagatzematge sense permisos + La trucada està sent enregistrada + Cancel·la l\'inici de la gravació + La gravació ha fallat. Poseu-vos en contacte amb el vostre administrador. + Iniciar l\'enregistrament + Realment voleu aturar l\'enregistrament? + Aturar l\'enregistrament de la trucada + Aturar l\'enregistrament + S\'està aturant l\'enregistrament… + Cal el consentiment de gravació per a totes les trucades + L\'enregistrament pot incloure la veu, el vídeo de la càmera i la compartició de pantalla. Es requereix el vostre consentiment abans d\'unir-vos a la trucada. Doneu el consentiment? + Requereix el consentiment per gravar abans d\'unir-te a la trucada en aquesta conversa + Consentiment de gravació + És possible que la trucada sigui gravada. + Enregistrament + S\'ha suprimit la conversa %1$s dels preferits + S\'ha canviat el nom de la conversa %1$s + Reinicialitza l\'estat + No és possible unir-se a altres sales mentre s\'està en una trucada + Desa + Scan QR Code + Sincronitza només amb servidors de confiança + Federat + Només visible per a les persones d\'aquesta instància i convidats + Local + Només visible per a les persones que coincideixen mitjançant la integració del número de telèfon a través de Talk al mòbil + Privat + Sincronitza amb servidors de confiança i amb la llibreta d\'adreces global i pública + Publicat + Commuta l\'abast + Canvia el nivell de privadesa de %1$s + Desplaça al final + Icona de Cerca + fa uns segons + Seleccionat + Enviar correu + Envia a + No teniu permís per compartir contingut amb aquest xat + Envia a … + Envia sense notificació + Estableix + Estableix l\'avatar des de la càmera + Estableix l\'estat + Estableix un missatge d\'estat + Compartir + Uneix-te a la conversa %1$s a %2$s + Àudio + Fitxer + Multimèdia + Altres + Enquesta + Enregistrament de trucades + Veu + Mostra el motiu de la prohibició + Mostra els participants prohibits + Preferits + No teniu permís per a iniciar una trucada + ha inciat una trucada + Missatge d\'estat + S\'ha revertit l\'estat + Canvia a la sala de descans + Canvia a la sala principal + Prendre una fotografia + S\'ha produït un error en fer la foto + No es pot fer una fotografia sense permisos + Torna a fer la fotografia + Envia + Canviar la càmera + Retalla la fotografia + Redueix la mida de la imatge + Commuta la llanterna + 30 minuts + Aquesta setmana + Això és un missatge de prova + Aquest cap de setmana + Resposta + Avui + Demà + Traducció + Traducció + Copia el text traduït + Detectar idioma + Paràmetres del dispositiu + No s\'ha pogut detectar la llengua + La traducció ha fallat + De + A + i 1 altre està escrivint… + estan escrivint… + està escrivint… + i %1$s més estan escrivint… + Desarxiva la conversa + Quan s\'hagi anul·lat l\'arxivament d\'una conversa, es tornarà a mostrar per defecte. + Anul·la la prohibició + Per llegir + Puja l\'avatar nou des del dispositiu + %1$s és fora de l\'oficina i és possible que no respongui. + %1$s és fora de l\'oficina avui + Substitució: + Utilitzar avatar + Adreça + Nom complet + Correu electrònic + Número de telèfon + Twitter + Lloc web + Estat + No s\'ha pogut recuperar la informació personal de l\'usuari. + No hi ha arranjament d\'informació personal + Afegiu el nom, foto i detalls de contacte a la vostra pàgina de perfil. + Videotrucada + Quin és el vostre estat? + + %d vot + %d vots + + diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml new file mode 100644 index 0000000..118e8de --- /dev/null +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -0,0 +1,728 @@ + + + Upravit + Přidat + Přidat do Poznámek + Konverzace %1$s přidána do oblíbených + Hledat v %s + Jevit se offline + Archivovat konverzaci + Jakmile je konverzace archivována, bude ve výchozím stavu skrytá. Pokud si chcete zobrazit archivované konverzace, vyberte filtr „Archivováno“. Přímá zmínění budou chodit i nadále. + Archivováno + Archivováno %1$s + Hlasový hovor + Bluetooth + Zvukový výstup + Telefon + Reproduktor + Drátová náhlavní souprava + Váš stav byl nastaven automaticky + Profilový obrázek + Pryč + Tlačítko zpět + Vyloučit + Vyloučit účastníka + Seznam vyloučení + Zaneprázdněn(a) + Kalendář + Pokročilé předvolby pro hovor + Hovor trvá už hodinu. + Zavolat bez upozornění + Oprávnění pro přístup ke kameře uděleno. Prosím vyberte kameru znovu. + Zrušit přihlášení + Vybrat profilový obrázek z cloudu + Vyčistit stavovou zprávu + Vyčistit stavovou zprávu po uplynutí + Zavřít + Ikona zavření + Spojení navázáno + Žádné spojení se serverem + Spojení ztraceno – odeslané zprávy jsou zařazené do fronty + Zamknout nahrávku pro nepřetržité nahrávání hlasové zprávy + Konverzace je archivována + Konverzace je pouze pro čtení + Nepodařilo se nastavit konverzaci na pouze pro čtení + Konverzace + Vytvořit konverzaci + Nahlásit problém + Uživatelsky určený + Nebezpečná zóna + %1$s v %2$s + Vymazat profilový obrázek + Smazat hlasovou nahrávku + Konverzace %1$s smazána + Nerušit + Nečistit + Upravit + Zprávy starší než 24 hodin není možné upravovat + Upravit zprávu + Nedávné + Šifrované + Ukončit hovor + Ukončit hovor pro všechny + Došlo k problému při načítání vašich chatů + Došlo k chybě při rušení vyloučení účastníka + Nepodařilo se uložit %1$s + 15 minut + složka + Načítání… + %1$s (%2$d) + Sledovaná vlákna + 4 hodiny + Nepodařilo se získat čekající pozvání + (upraveno) + Interní poznámka + Není vidět + Jazyky se nepodařilo získat + Získání se nezdařil + Později dnes + Opustit hovor + Opustili jste konverzaci %1$s + Načíst další výsledky + Místní čas: %1$s + Uzamknout konverzaci + Symbol zámku + Přestat se hlásit + Konverzace %1$s označena jako přečtená + Konverzace %1$s označena jako nepřečtená + Zmíněni + Nejnovější jako první + Nejstarší jako první + A - Z + Z - A + Největší jako první + Nejmenší jako první + Zpráva zkopírována + Zprávu jste smazali + Upraveno %1$s + Klepnutím anketu otevřete + Nic nenalezeno + Hledejte psaním… + Hledat… + Zprávy + Zvolený účet byl naimportován a je k dispozici + O aplikaci + Aktivní uživatel + Přidat účet + Účet je naplánován ke smazání a není možné ho změnit + Otevřít hlavní nabídku + Přidat přílohu + Přidat emotikony + Přidat do konverzace + Přidat účastníky + Přidat do oblíbených + OK, vše hotovo! + Pin: %1$s + Odemknout %1$s + Pro použití bluetooth reproduktorů je třeba udělit oprávnění „Zařízení poblíž“. + Zvednou videohovor + Zvednout pouze jako hlasový + Změnit zvukový výstup + Vyp/zap. kameru + Zavěsit + Vyp/zap. mikrofon + Otevřít v režimu obraz-v-obraze + Přepnout na video sama sebe + PŘÍCHOZÍ + Název konverzace + Upozornění na hovor + %1$s se hlásí + Znovunavazování spojení… + ZVONĚNÍ + %1$s má hovor + %1$s přes telefon + %1$s s přenosem obrazu + Žádná odezva po 45 sekundách, ťukněte, pokud chcete zkusit znovu + %s hovor + %s video hovor + %s hlasový hovor + Pro zapnutí videokomunikace udělte oprávnění „Kamera“ . + Storno + Nepodařilo se získat schopnosti, přerušuje se + Titulek + Věříte doposud neznámému SSL certifikátu, vydaném %1$s pro %2$s, platnému od %3$s do %4$s? + Zkontrolovat certifikát + Vaše nastavení SSL zabránilo připojení + Změnit certifikát sloužící pro ověřování se + Změnit heslo + Zrušit upravování + Zrušit upravování + Smazat všechny zprávy + Všechny zprávy byly vymazány + Opravdu chcete všechny zprávy v této konverzaci smazat? + Změnit klientský certifikát + Nastavit klientský certifikát + a + Zkopírovat + Zkopírováno do schránky + Vytvářet + Vypnuto + Zavřít + Omlouváme se, něco se pokazilo! + Další volby + Nastavit + Přeskočit + Neznámé + Vyberte certifikát pro ověřování se + Připojování… + Hotovo + Popis konverzace + Informace o konverzaci + Videohovor + Hlasový hovor + Konverzace nenalezena + Nastavení konverzace + Připojte se ke konverzaci, nebo začněte novou + Pozdravte své přátele a kolegy! + Zkopírovat + Vytvořit novou konverzaci + Vytvořit anketu + Vy: + Dnes + Včera + Smazat + Smazat vše + Smazat konverzaci + Pokud konverzaci smažete, bude smazána také pro všechny její ostatní účastníky. + Smazat zprávu + Zpráva úspěšně smazána, ale možná unikla do jiných služeb + Smazat nyní + Uživatel %1$s byl odebrán + Odebrat oprávnění moderátora + Nahrát hlasovou zprávu + Poslat zprávu + Stávající účet + Server + Je nainstalovaná aplikace upozorňování na serveru? + Uživatel + Je zapnutý stav uživatele? + Verze systému Android + Aplikace + Název aplikace + Registrovaní uživatelé + Verze aplikace + Optimalizace pro nízkou spotřebu energie z akumulátoru je ignorována, všechno v pořádku + Šetření energií z akumulátoru může způsobovat problémy. Měli byste ho vypnout! + Nastavení správy napájení + Zařízení + Otevřít kontrolní seznam řešení problémů + Otevřít diagnostickou obrazovku + Otevřít dontkillmyapp.com + Stažení nejnovějšího push tokenu firebase + Vytvoření nejnovějšího push tokenu firebase + Nenastaven žádný push token firebase. Prosím vytvořte hlášení chyby. + push token Firebase + Služby Google Play nejsou k dispozici. Upozorňování proto nejsou podporována + Služby Google Play + Služby Google Play jsou k dispozici + Poslední push registrace na push proxy + Zatím nezregistrováno u push proxy + Poslední push registrace na serveru + Ještě nezaregistrováno na serveru + Metainformace + Vytváření výkazu o systému + Kanál upozorňování na hovory zapnut? + Kanál upozorňování na zprávy zapnut? + Oprávnění upozornění + Telefon + Verze Talk na serveru + Verze serveru + Externí + Vnitřní + Režim signalizování + Neplatné heslo + Server se nachází v režimu údržby. + Aplikace je zastaralá + Tato aplikace je příliš stará a už nepodporovaná tímto serverem. Prosíme aktualizujte ji. + Aktualizovat + Chcete tento účet znovu ověřit nebo smazat? + Uložení tohoto média na úložiště umožní ostatním aplikacím na vašem zařízení k němu přistupovat. + Pokračovat? + Ne + Uložit na úložiště? + Ano + Nepodařilo se získat zobrazované jméno, přerušuje se + Nepodařilo se uložit zobrazované jméno, přerušuje se + Upravit + Upravit + Upravit zprávu + Upraveno správcem + Nabídka konverzace k události + Plán + 8 hodin + 4 týdny + Vypnuto + 1 den + 1 hodina + 1 týden + Ukončit platnost zpráv v chatu + Platnost zpráv v chatu je možné ukončit po uplynutí zadané doby. Pozn.: Soubory nasdílené v chatu nebudou vlastníkovi smazány, ale už nadále nebudou sdíleny v konverzaci. + Nepodařilo se získat nastavení signalizace + Přijmout + Zamítnout + od %1$s v %2$s + Žádné nevyřízené pozvánky + Máte nevyřízené pozvánky + Zpět + Je zapotřebí oprávnění pro přístup k souborům + Filtrovat konverzace + Uživatel následující veřejný odkaz + Vy: %1$s + Přeposlat + Přeposlat na… + Galerie + Nemáte ještě server?\nKlepnutím sem ho získejte od poskytovatele + Získat zdrojové kódy + Group + Host + Přístup pro hosty + Nedaří se zapnout/vypnout přístup hostům. + Umožnit hostům sdílet veřejný odkaz pro připojení do konverzace + Umožnit hosty + Zadejte heslo + Heslo pro přístup hosta + Chyba při nastavování/vypínání hesla + Nastavit heslo pro omezení přístupu k veřejnému odkazu. + Ochrana heslem + Znovu poslat pozvánky + Pozvánka nebyla odeslána kvůli chybě. + Pozvánky byly znovu odeslány. + Sdílet odkaz na konverzaci + Zadejte zprávu… + Úspora energie z akumulátoru není ignorována. Pokud chcete, aby upozorňování na pozadí fungovalo správně, mělo by toto být změněno! Klikněte na OK a vyberte „Všechny aplikace“ -> %1$s -> neoptimalizovat + Ignorovat úsporu energie z akumulátoru + Důležitá konverzace + Stav uživatele „Nevyrušovat“ bude v případě důležitých konverzací ignorován + Neplatný čas + Pozvání + Přidat se do všem přístupných konverzací + Ponechat + Než budete moci konverzaci opustit, je třeba předat někomu roli moderátora + %1$s | Naposledy upraveno: %2$s + Opustit konverzaci + Opouštění hovoru… + GNU General Public License, verze 3 + Licence + byl dosažen limit %s znaků + Čekárna + Tato schůzka je naplánována na %1$s + Schůzka brzy začne + V tuto chvíli se nacházíte v čekárně. + Vaše stávající umístění + vyžadováno oprávnění pro přístup k údaji o poloze + Pozice neznámá + Uzamčeno + Odemkněte klepnutím + Nenastaveno + Označit jako přečtené + Označit jako nepřečtené + Konverzace označena jako důležitá + Zrušeno značení konverzace coby citlivé + Konverzace označena jako citlivá + Zrušeno značení konverzace coby důležité + Schůzka skončila + Zpráva přidána do poznámek + Nezdařilo se + Odeslání zprávy se nezdařilo: + Bez připojení + Odebrat odpověď + Zpráva přečtena + Odesílání + Zpráva odeslána + Mikrofon je povolený a zvuk se nahrává + Pro zapnutí hlasové komunikace udělte oprávnění „Mikrofon“ . + Zmeškali jste hovor od %s + Moderátor + Nová konverzace + Viditelnost + Nepřečtená zmínění + Nepřečtené zprávy + %1$s není k dispozici (nenainstalováno nebo administrativně omezen přístup k) + Host + Ne + Žádné otevřené konverzace + Žádné otevřené konverzace, ke kterým byste se mohli přidat.\nBuď zde žádné takové nejsou nebo už jste se přidali do všech. + Bez proxy + Nemáte oprávnění aktivovat zvuk! + Nemáte oprávnění aktivovat video! + Teď ne + %1$s v %2$s kanálu upozornění + Volání + Oznámit příchozí volání + Zprávy + Oznámit příchozí zprávy + Nahrané + Upozorňovat ohledně postupu při nahrávání + Nastavení upozorňování + Notifikace nejsou správně nastavené + Oprávnění k upozorňování a nastavení ohledně spotřeby energie z akumulátoru jsou nastavené správně pro dostávání upozornění. Pokud i tak máte problémy s dostáváním notifikací, zkontrolujte zda jsou zapnuté kanály upozorňování na hovory a zprávy. Další pomoc je možné nalézt na DontKillMyApp.com nebo v kontrolním seznamu řešení problémů. Pokud toto nepomůže, přejděte na diagnostickou obrazovku a zašlete hlášení chyby. + Řešení problémů s upozorňováním + Vždy upozorňovat + Upozornit, když vás někdo zmíní + Nikdy neupozorňovat + Jste bez připojení – zkontrolujte svou konektivitu + OK + Probíhající schůzka + Otevřít konverzaci registrovaným uživatelům + Otevřít také pro neregistrované hosty + Vlastník + Účastníci + Přidat účastníky + Heslo + Nastavit oprávnění + Některá oprávnění byla odepřena. + Udělte oprávnění + Otevřít nastavení + Udělte oprávnění v Nastavení > Oprávnění + Účet nenalezen + Rozhovor prostřednictvím %s + Ztlumit mikrofon + Zapnout mikrofon + Zprávy + Soukromí + Osobní údaje + Povýšit na moderátora + Veřejná konverzace + Push upozorňování vypnutá + Omlouváme se, něco se pokazilo – chyba je %1$s + Omlouváme se, něco se pokazilo – nebylo možné stáhnout si testovací push zprávu + Push upozornění úspěšně odesláno. Nyní byste měli obdržet upozornění na tomto zařízení s nadpisem „Testování push upozorňování“ + Vysílačka + Při vypnutém mikrofonu, klikněte a dokud držíte, můžete mluvit – jako u vysílačky + Připomenout později + Odebrat z oblíbených + Odebrat skupinu a její členy + Odebrat účastníka + Odebrat heslo + Odebrat tým a uživatele + Přejmenovat konverzaci + Přejmenovat + Odpovědět + Odpovědět soukromě + Místnost je úspěšně ponechána + Uložit + Úspěšně uloženo + 30 sekund + 5 minut + 1 minuta + 10 minut + Okamžitě + 600 + 60 + 30 + 300 + Hledat + Vyčistit hledání + Vyberte účet + Aktualizovat zprávu + Odeslat hlasovou nahrávku + Citlivá konverzace + Náhled zprávy bude vypnut pro seznam konverzací a notifikace + %1$s poslal(a) GIF animaci. + Odeslali jste GIF animaci. + %1$s poslal(a) video. + Odeslali jste video. + %1$s poslal(a) zvuk. + Odeslali jste zvuk. + %1$s poslal(a) obrázek. + Odeslali jste obrázek. + %1$s poslal(a) kartu aplikace Deck + Vyzkoušet připojení k serveru + Přejděte na novější verzi databáze %1$s + Zvolený účet se nepodařilo naimportovat + Odkaz na webové rozhraní vámi využívané instance %1$s, když ho otevíráte ve webovém prohlížeči. + Importovat účet z aplikace %1$s + Importovat účet + Importovat účty z aplikace %1$s + Importovat účty + Přepněte svůj %1$s zpět z režimu údržby + Dokončete instalaci %1$s + Zkouší se připojení + Na serveru není nainstalovaná podporovaná verze aplikace Talk + Adresa serveru https://… + %1$s funguje pouze s %2$s 13 a novějším + Nastavit nové heslo + Nastavit heslo + Nastavení + Namísto přidání nového byl zaktualizován váš stávající účet + Pokročilé + Vzhled + Volání + Obraťte se na správce + Otevřít diagnostickou obrazovku pro kontrolu nastavení nebo vytvoření hlášení chyby + Diagnostika + Dát klávesnici pokyn, že má vypnout přizpůsobené učení se (bez záruky) + Inkognito klávesnice + Žádný zvuk + Aplikace Talk není na serveru, vůči kterému jste se pokusili ověřit, vůbec nainstalována + Upozornění + Upozorňování jsou odmítnuta + Upozorňování jsou udělena + Zprávy + Najít kontakty se stejným telefonním číslem a přidat zkratku Talk do systémové aplikace pro kontakty + Chyba 429 – příliš mnoho požadavků + Můžete si nastavit své telefonní číslo a tak vás budou moci ostatní uživatelé najít + Zadejte telefonní číslo + Neplatné telefonní číslo + Nastavení telefonního čísla úspěšné + Telefonní číslo + Propojení telefonních čísel + Soukromí + Hostitel proxy + Heslo do proxy + Port proxy + Typ proxy + Uživatelské jméno pro proxy + Sdílet mé oznámení o přečtení a zobrazit oznámení o přečtení ostatních + Oznámení o přečtení + Znovu získat pověření účtu + Odebrat + Odebrat účet + Potvrďte, že chcete stávající účet odebrat. + Uzamknout %1$s pomocí zámku obrazovky Android nebo podporované biometrické metody + Časový limit nečinnosti pro zámek obrazovky + Zámek obrazovky + Brání pořizování snímků obrazovky v seznamu nedávných a uvnitř aplikace + Zabezpečení obrazovky + Verze serveru je příliš stará a v příštím vydání této aplikace už nebude podporována! + Verze serveru je příliš stará a nepodporovaná touto verzí aplikace pro Android + Nepodporovaný server + Serverová aplikace pro notifikace není nainstalovaná + Nastaveno úsporou akumulátoru + Tmavý + Použít systémový výchozí + motiv vzhledu + Světlý + Motiv vzhledu + Sdílet můj stav píše a zobrazovat stav píší u ostatních + Stav psaní je k dispozici pouze pokud je používána podpůrná vrstva pro vysoký výkon (HPB) + Stav psaní + Proxy vyžaduje přihlašovací údaje + Varování + Pověření může být znovuzískáno pouze pro stávající účet + Sdílet kontakt + Je třeba oprávnění ke čtení kontaktů + Sdílet stávající umístění + Nasdílet odkaz + Sdílet umístění + Sdílet toto umístění + Zvolte účet + Sdílené položky + Karta aplikace Deck + Obrázky, soubory, hlasové zprávy… + Žádné sdílené položky + Poloha + Sdílené umístění + Pokud notifikace nejsou správně nastavené, zobrazit pravidelné varování + Zobrazovat pravidelné varování ohledně notifikací + Řadit podle + Zahájit skupinový chat + Čas zahájení + Přepnout účet + Tým + Vyzkoušet push notifikace + Výsledky testu + Dnes v%1$s + Zítra v %1$s + Vyberte soubory + Odeslat tyto soubory k %1$s? + Odeslat tento soubor k %1$s? + Odesílání se nezdařilo, omlouváme se + Nepodařilo se nahrát %1$s + Nezdar + Nasdílet z %1$s + Nahrát ze zařízení + Nahrávání + %1$s po %2$s %3$s\%% + Vyfotit + Natočit video + Uživatel + Videozáznam z %1$s + Nahrávka z Talk z %1$s (%2$s) + Podržením nahrajte, uvolněním odešlete. + Je zapotřebí oprávnění pro zaznamenávání zvuku + « Zrušíte přejetím prstem + Webinář + Ano + Příští týden + Žádné archivované konverzace + Neuloženy žádné zprávy pro režim bez připojení + Není možné propojit telefonní čísla, protože chybí potřebná oprávnění + Všechny zprávy + Pouze zmínky ve stylu @jméno + Vypnuto + Výchozí + Nastavení následování konverzace + 1 hodina + Online + Stav online + Otevřít konverzace + Otevřít v aplikaci Soubory + Otevřít poznámky + Přejít na vlákno + Přehrát/pozastavit hlasovou zprávu + Ovládání rychlosti přehrávání + Přidat volbu + Upravit hlas + Ukončit anketu + Opravdu chcete tuto anketu ukončit? Toto nelze vzít zpět. + V této anketě není možné hlasovat více volbami. + Vícero odpovědí + Smazat volbu %1$d + Volba %1$d + Možnosti + Soukromá anketa + Otázka + Vaše otázka + Výsledky + Nastavení + Hlasovat + Hlas odeslán + Dříve nastavené + QR kód se nepodařilo načíst + Hlásit se + Vše + Sdílení souborů z úložiště není možné bez oprávnění + Nedávná vlákna + Hovor je nahráván + Zrušit zahajování nahrávání + Nahrávání se nezdařilo. Obraťte se na svého správce. + Zahájit nahrávání + Opravdu chcete přestat nahrávat? + Zastavit nahrávání hovoru + Zastavit nahrávání + Zastavování nahrávání + Souhlas s nahráváním je vyžadován pro všechna volání + Nahrávka může obsahovat váš hlas, obraz z kamery a sdílení obrazovky. K hovoru se budete moci připojit až po odsouhlasení. Souhlasíte? + Vždy vyžadovat souhlas s nahráváním před připojením hovoru v této konverzaci. + Souhlas s nahráváním + Hovor může být nahráván. + Zaznamenávání + Konverzace %1$s odebrána z oblíbených + Konverzace %1$s byla přejmenována + Poslat znovu + Resetovat stav + Během hovoru se nelze připojit k jiným místnostem. + Uložit + Naskenovat QR kód + Synchronizovat pouze s důvěryhodnými servery + Propojené + Viditelné pouze lidem na této instanci a hostům + Místní + Viditelné pouze lidem, se kterými nalezena shoda začleněním přes telefonní číslo prostřednictvím Talk na mobilním telefonu + Soukromé + Synchronizovat s důvěryhodnými servery a globálním a veřejným adresářem kontaktů + Zveřejněno + Přepnout pohled + Změnit úroveň soukromí pro %1$s + Odrolovat na konec + Ikona vyhledávání + sekund před + Vybráno + Odeslat e-mail + Odeslat k + Nemáte oprávnění sdílet obsah do tohoto chatu + Odeslat k… + Odeslat bez upozornění + Nastavit + Získat zástupný obrázek z kamery + Nastavit stav + Nastavit stavovou zprávu + Sdílet + Připojit se ke konverzaci %1$s na %2$s + Zvuk + Soubor + Média + Ostatní + Anketa + Nahrávka hovoru + Hlas + Zobrazit důvod vyloučení + Zobrazit vyloučené účasníky + Oblíbené + Nemáte oprávnění pro zahájení hovoru + Vytvořit vlákno + zahájen hovor + Stavová zpráva + Stav vrácen na původní + Přepnout na přestávkovou místnost + Přepnout na hlavní místnost + Vyfotit + Chyba při pořizování obrázku + Focení není možné bez udělení oprávnění + Znovu pořídit fotku + Odeslat + Přepnout kameru + Oříznout fotku + Zmenšit velikost obrázku + Vyp/zap. přisvícení + 30 minut + Tento týden + Toto je zkušební zpráva + Tento víkend + Zrušit vytváření vlákna + Notifikace ohledně vlákna + Odpověď + Název vlákna + Dnes + Zítra + Překládání + Překlad + Zkopírovat přeložený text + Zjistit jazyk + Nastavení zařízení + Nepodařilo se zjistit jazyk + Překlad se nezdařil + Od + Pro + a 1 další píše… + píší… + píše… + a %1$s ostatní píší… + Zrušit archivaci konverzace + Jakmile je zrušeno archivování konverzace, znovu bude zobrazováná už ve výchozí stavu. + Zrušena archivace %1$s + Zrušit vyloučení + Nastavit jako nepřečtené + Nahrát nový profilový obrázek ze zařízení + %1$s je mimo kancelář a může se stát, že neodpoví. + %1$s je dnes mimo kancelář + Nahrazení: + Zástupný obrázek uživatele + Adresa + Celé jméno + E-mail + Telefonní číslo + Twitter + Webová stránka + Stav + Nepodařilo se získat osobní údaje uživatele. + Osobní údaje nejsou nastaveny + Přidejte jméno, obrázek a kontaktní údaje na svou profilovou stránku. + Video hovor + Jaký je váš stav? + + Viz %d podobná zpráva + Viz %d podobné zprávy + Viz %d podobných zpráv + Viz %d podobné zprávy + + + Tato konverzace bude automaticky smazána pro kohokoli po %1$d dni bez jakékoli aktivity. + Tato konverzace bude automaticky smazána pro kohokoli po %1$d dnech bez jakékoli aktivity. + Tato konverzace bude automaticky smazána pro kohokoli po %1$d dnech bez jakékoli aktivity. + Tato konverzace bude automaticky smazána pro kohokoli po %1$d dnech bez jakékoli aktivity. + + + %d odpověď + %d odpovědi + %d odpovědí + %d odpovědi + + + %d hlas + %d hlasy + %d hlasů + %d hlasy + + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml new file mode 100644 index 0000000..105a49c --- /dev/null +++ b/app/src/main/res/values-da/strings.xml @@ -0,0 +1,659 @@ + + + Redigér + Tilføj + Tilføj til Noter + Tilføjede samtalen %1$s til favoritter + Søg i %s + Er offline + Arkivér samtale + Når en samtale er arkiveret, vil den blive skjult som standard. Vælg filteret \"Arkiveret\" for at se arkiverede samtaler. Direkte omtaler vil stadig blive modtaget. + Arkiveret + Arkiverede %1$s + Bluetooth + Lydudgang + Telefon + Højtaler + Kablet headset + Status sat automatisk + Avatar + Ikke tilstede + Tilbage knap + Bloker + Bloker deltager + Liste over blokerede + Optaget + Kalender + Avancerede opkaldsmuligheder + Opkaldet har været igang i en time. + Ring uden notifikation + Kameratilladelse er givet. Vælg kamera igen. + Annullér log på + Vælg avatar fra skyen + Ryd status notifikation + Ryd status notifikationer efter + Luk + Luk ikon + Forbindelse oprettet + Forbindelse mistet - Sendte beskeder er i kø + Lås optagelse for kontinuerlig optagelse af talebeskeden + Samtalen er skrivebeskyttet + Samtalen kunne ikke indstilles som skrivebeskyttet + Samtaler + Opret samtale + Opret problem + Brugerdefineret + Farezone + %1$s i %2$s + Slet avatar + Slettede samtale %1$s + Forstyr ikke + Ryd ikke + Redigér + Nylige + Krypteret + Afslut opkald for alle + Der opstod et problem med at indlæse dine chats + En fejl opstod under fjernelse af blokering af deltager + Kunne ikke gemme %1$s + 15 minutter + mappe + Loading … + %1$s (%2$d) + 4 timer + Kunne ikke hente afventende invitationer + (redigeret) + Intern bemærkning + Usynlig + Sprog kunne ikke hentes + Hentning fejlede + Senere i dag + Forlad opkald + Du forlod samtalen %1$s + Indlæs flere resultater + Lås samtale + Låsesymbol + Sænk hånden + Marker samtalen %1$s som læst + Marker samtalen %1$s som ulæst + Nævnt + Nyeste først + Ældste først + A - Å + Å - A + Største først + Mindste først + Besked slettet af dig + Redigeret af %1$s + Rør for at åbne afstemning + Ingen søgeresultater + Start indtastning for at søge ... + Søg ... + Beskeder + Vis ikke notifikationer + Den valgte konto blev importeret og kan bruges nu + Om + Aktiv bruger + Tilføj konto + Kontoen er planlagt til sletning, og kan ikke ændres + Åben hovedmenu + Tilføj vedhæftelse + Tilføj emojis + Tilføj til samtale + Tilføj deltagere + Føj til favoritter + OK, alt er klar! + Fastgør: %1$s + Lås op %1$s + For at aktivere bluetooth højtalere så giv venligst tilladelse til \"Apparater i nærheden\". + Svar som videokald + Svar kun som stemmekald + Skift audiooutput + Skift kamera + Læg på + Skift mikrofon + Åben billede-i-billede tilstand + Skift til egen video + INDKOMMENDE + Samtalenavn + Opkaldsnotifikationer + %1$s løftede hånden + Genforbinder ... + RINGER + %1$s i opkald + %1$s med telefon + %1$s med video + Intet respons i 45 sekunder, rør skærmen for at forsøge igen + %s opkald + %s videoopkald + %s stemmeopkald + For at aktivere videokommunikation, så giv venligst tilladelse til \"Kamera\". + Annuller + Anskaffelse af evner fejlede, annullerer + Overskrift + Stoler du på det, indtil nu, ukendte SSL certifikat ,udstedt af %1$s for %2$s, gyldig fra %3$s til %4$s? + Tjek certifikatet + Din SSL indstilling forhindrede forbindelse + Skift godkendelsescertifikat + Skift adgangskode + Slet alle beskeder + Alle beskeder blev slettet + Ønsker du virkelig at slette alle beskeder i denne samtale? + Skift kliencertifikat + Indstil klientcertifikat + og + Kopier + Kopieret til udklipsholder + Opret + Deaktiveret + Afvis + Noget gik galt, undskyld! + Flere muligheder + Sæt + Spring over + Ukendt + Vælg godkendelsescertifikat + Forbinder ... + Færdig + Samtalebeskrivelse + Samtaleinfo + Videoopkald + Stemmeopkald + Samtalen blev ikke fundet + Samtale indstillinger + Deltag i en samtale eller start en ny + Sig hej til dine venner og kollegaer. + Kopier + Opret ny samtale + Opret afstemning + I dag + I går + Slet + Slet alt + Slet samtale + Hvis du sletter konversationen, vil den også blive slettet for alle andre deltagere. + Slet besked + Besked slettet, men kan kan være lækket til andre services + Brugeren %1$s blev fjernet + Degrader fra moderator + Optag stemmebesked + Send besked + Nuværende konto + Server + Er server notifikations app\'en installeret? + ruger + Brugerstatus aktiveret? + Android version + App + App navn + Registreret bruger + App version + Batterioptimering er ignoreret, alt er fint + Batterioptimering er aktiveret, hvilket kan medføre problemer. Du bør deaktivere batterioptimering! + Batteriindstillinger + Enhed + Åben fejlsøgnings kontrolliste + Åben diagnosticeringsskærm + Åben dontkillmyapp.com + Seneste firebase push token hentning + Seneste firebase push token generering + Intet firebase push token sat. Opret venligst en fejlrapport. + Firebase push token + Google Play services er ikke tilgængelige. Notifikationer er ikke understøttet + Google Play services + Google Play services er ikke tilgængelige + Seneste push registrering på push proxy + Endnu ikke registreret på push proxy + Seneste push registrering på server + Endnu ikke registreret på server + Metainformation + Generering af systemrapport + Kanal til opkaldsnotifikationer aktiveret? + Kanal til beskednotifikationer aktiveret? + Notifikationstilladelser + Telefon + Server Snak version + Server version + Ekstern + Intern + Signaleringstilstanding Mode + Ugyldigt kodeord + Server er aktuelt i vedligeholdelsestilstand. + App\'en er forældet is outdated + Denne app er for gammel og understøttes ikke længere af denne server. Opdater venligst. + Opdatér + Ønsker du at genautorisere eller slette denne konto? + Hvis du gemmer dette medie i dit lager, så vil enhver anden app på dit apparat kunne tilgå det. + Fortsæt? + Nej + Gem til lager? + Ja + Display navn kunne ikke hentes, afbrydes + Kunne ikke opbevare skærmnavn, afbryder + Redigér + Redigér + Redigeret af admin + 8 timer + 4 uger + Slået fra + 1 dag + 1 time + 1 uge + Lad chatbeskeder udløbe + Chatbeskeder kan udløbe efter et vist tidsrum. Bemærk: Filer der er delt i chatten vil ikke blive slette for ejeren, men vil ikke længere deles i samtalen. + Anskaffelse af signaleringsindstillinger fejlede + Accepter + Afvis + fra %1$s den %2$s + Ingen afventende invitationer + Du har afventende invitationer + Tilbage + Tilladelse til filadgang er krævet + Bruger følger et offentligt link + Dig: %1$s + Videresend + Forward til ... + Galleri + Har du ikke en server endnu?\nKlik her for at skaffe en udbyder + Hent kildekode + Gruppe + Gæst + Gæsteadgang + Kan ikke aktivere/deaktivere gæsteadgang. + Tillad gæster at dele et offentligt link for at deltage i denne samtale. + Tillad gæster + Angiv en adgangskodea password + Gæsteadgang adgangskode + Fejl under angivelse/deaktivering af adgangskode. + Angiv en adgangskode for at begrænse hvem der kan anvende det offentlige link. + Adgangskodebeskyttelse + Gensend invitationer + Invitationer blev ikke sendt pga en fejl. + Invitationer blev udsendt igen. + Del samtalelink + Angiv en besked ... + Batterioptimering bliver ikke ignoreret. Dette bør ændres for at være sikker på at notifikationer virker i baggrunden! Klik venligst på OK og vælg \"Alle apps\" -> %1$s -> Optimer ikke + Ignorer batterioptimering + Vigtig samtale + Invitationer + Deltag i åbne samtaler + Du skal udnævne en ny moderator inden du kan forlade samtalen. + %1$s| Sidst ændret: %2$s + Forlad samtale + Forlader opkaldet ... + GNU General Public License, Version 3 + Licens + %skarakterbegrænsning nået + Lobby + Mødet er skemalagt til %1$s + Mødet starter snart + Du venter nu i lobbyen. + Din aktuelle placering + Placeringsrettighed krævet + Placering ukendt + Låst + Tryk for at låse op + Ikke indstillet + Marker som læst + Marker som ulæst + Mislykkede + Kunne ikke sende besked: + Offline + Annuller svar + Besked læst + Sender + Beskeden blev sendt + For at aktivere kommunikation så tillad venligst \"Mikrofon\" + Du missede et opkald fra %s + Moderator + Ny samtale + Synlighed + Ulæste omtaler + Ulæste beskedder + %1$s ikke tilgængelig (Ikke installeret eller begrænset af admin) + Gæst + Nej + Ingen åbne samtaler + Ingen åbne samtaler som du kan deltage i.\nEnten er der ingen åbne samtaler eller du deltager allerede i alle. + Ikke proxy + Du har ikke tilladelse til at aktivere audio! + Du har ikke tilladelse til at aktivere video! + Ikke nu + %1$s på %2$s notifikationskanal + Opkald + Underret om indkomne opkald + Beskeder + Underret om indkomne beskeder + Uploadede filer + Underret om upload fremskridt + Meddelelsesindstillinger + Notifikationer er ikke opsat korrekt + Notifikationstilladelse og batteriindstilling er ikke korrekt sat op til at modtage notifikationer. Hvis du har problemer med at modtage notifikationer overhovedet, så kontroller venligst om notifikationskanalerne for opkald og beskeder er aktiveret. Yderligere hjælp kan findes på DontKillMyApp.com eller på fejlsøgningslisten. Hvis dette ikke hjælper, så gå venligst til diagnosticeringsskærmen og indsend en fejlrapport. + Fejlsøgning af notifikationer + Giv altid besked + Giv besked når du bliver omtalt + Giv aldrig besked + For nuværende offline, venligst kontroller din forbindelse + OK + Begynd en samtale til registrerede brugere + Også åben for gæste app brugere + Ejer + Deltagere + Tilføj deltagere + Adgangskode + Angiv rettigheder + Nogle rettigheder blev nægtet. + Tillad venligst rettigheder + Indstillinger + Giv venligst rettigheder under Indstillinger > Rettigheder + Konto blev ikke fundet + Chat via %s + Mute mikrofon + Aktiver mikrofon + Beskeder + Privatliv + Personlige oplysninger + Forfrem til moderator + Offentlig samtale + Pushbeskeder er slået fra + Tryk-for-at-tale + Med mikrofonen deaktiveret, hold&nede for at bruge Tryk-for-at-tale + Påmind mig senere + Fjern fra favoritter + Fjern gruppe og medlemmer + Fjern deltager + Fjern adgangskode + Fjern team og medlemmer + Omdøb samtale + Omdøb + Besvar + Svar privat + Gem + Gemt + 30 sekunder + 5 minutter + 1 minut + 10 minutter + Straks + 600 + 60 + 30 + 300 + Søg + Ryd søgning + Vælg konto + %1$s sendte en GIF. + Du sendte en GIF. + %1$s sendte en video. + Du sendte en video. + %1$s sendte en lyd. + Du sendte en lyd. + %1$s sendte et billede. + Du sendte et billede. + %1$s sendte et opslagskort + Test server forbindelsen + Opdater venligst din %1$s database + Den valgte konto kunne ikke importeres + Linket til dit %1$s web interface når du åbner den i browseren. + Importer konto fra %1$s appen + Importér konto + Importer konti fra %1$s appen + Importér konti + Få venligst %1$s ud fra vedligeholdelse + Afslut venligst din %1$s installation + Tester forbindelsen + Server har ikke understøttet Snak app installeret + Serveradresse https://… + %1$svirker kun med %2$s 13 og nyere + Indstil ny adgangskode + Angiv adgangskode + Indstillinger + Vi har opdateret din eksisterende konto i stedet for at tilføje en ny + Avanceret + Udseende + Opkald + Kontakt venligst administratoren for + Åben diagnosticeringsskærmen for at kontrollere indstillinger og oprette en fejlrapport + Diagnose + Indstiller tastatur til at deaktivere personaliseret indlæring (uden garanti) + Inkognito tastatur + Ingen lyd + Snak appen er ikke installeret på serveren du prøver at blive godkendt på + Notifikationer + Notifikationer er afvist + Notifikationer er tilladte + Beskeder + Match kontakter baseret på telefonnummer for at integrere. Snak genvej til system kontakt app + Fejl429 For mange forespørgsler + Du kan angive dit telefonnummer så andre brugere kan finde dig + Indtast telefonnummer + Ugyldigt telefonnummer + Telefonnummer angivet + Telefonnummer + Telefonnummer integration + Privatliv + Proxy host + Proxy adgangskode + Proxy port + Proxy type + Proxy brugernavn + Del min læsestatus og vis andres læsestatus + Læse status + Genautoriser konto + Fjern + Fjern konto + Bekræft venligst din hensigt til at fjerne den aktuelle konto. + Lås %1$s med Android Skærm lås en understøttet biometrisk metode + Skærm lås inaktivitets timeout + Skærm lås + Forhindrer skærmkopi i nyeste liste og i selve app-en + Skærm sikkerhed + Serverversionen er meget gammel og vil ikke være understøttet i den næste udgivelse! + Serverversionen er for gammel og er ikke understøttet af denne version af Android app\'en + Ikke understøttet server + Server notifikations app ikke installeret + Mørk + Brug system default + tema + Lys + Tema + Del min indtastningsstatus og vis indtastningsstatus fra andre + Indtastningsstatus er kun tilgængelig når der anvendes en højtydende backend (HPB) + Indtastningsstatus + Proxy kræver legitimationsoplysninger + Advarsler + Kun nuværende konto kan gen autoriseres + Del kontakt + Rettighed til at læse kontakter er krævet + Del aktuel placering + Del link + Del placering + Del denne placering + Vælg konto + Delte elementer + Opslagskort + Billeder, filer, stemmebeskeder ... + Ingen delte elementer + Sted + Delt placering + Når notifikationer ikke er opsat korrekt, så vis en regulær advarsel + Vis regulær notifikationsadvarsel + Sorter efter + Start gruppechat + Start tid + Skift konto + Team + Vælg filer + Send disse filer til %1$s? + Send denne fil til %1$s? + Desværre, upload fejlede + Kunne ikke uploade %1$s + Fejl + Deling fra %1$s + Upload fra enhed + Uploader + %1$s til %2$s - %3$s\%% + Tag et billede + Optag video + ruger + Videooptagelse fra %1$s + Snak optagelse fra %1$s (%2$s) + Hold for at optage, slip for at sende. + Rettighede til audiooptagelse er krævet + « Rul for at annullere + Webinar + Ja + Næste uge + Ingen offline beskeder gemt + Ingen telefonnummerintegration på grund af manglende rettigheder + Kun omtalt med @ + Deaktivér + Standard + 1 time + Online + Online status + Åbne samtaler + Åben i appe\'en filer + Afspil/pauser stemmebesked + Afspilningshastighedskontrol + Tilføj valg + Redigér stemme + Afslut afstemning + Ønsker du virkelig at afslutte denne afstemning? Dette kan ikke fortrydes. + Du kan ikke stemme med flere muligheder i denne afstemning. + Multiple svar + Slet mulighed %1$d + Mulighed %1$d + Valgmuligheder + Privat afstemning + Spørgsmål + Dit spørgsmål + Resultater + Indstillinger + Stemme + Stemme indsendt + Tidligere sat + Løft hånden + Alle + Deling af filer fra lager er ikke muligt uden rettigheder + Opkaldet optages + Annuller opstagelsesstart + Optagelsen fejlede. Kontakt venligst din administrator + Start optagelse + Ønsker du virkelig at stoppe optagelsen? + Stop opkaldsoptagelse + Stop optagelse + Stopper optagelse ... + Optagelsessamtykke er kræver for alle opkald + Optagelsen kan indeholde din stemme, video fra kamera, og skærmdeling. Dit samtykke kræves inden du deltager i opkaldet. Ønsker du at samtykke? + Kræv optagelsessamtykke inden deltagelse i opkaldet i denne samtale + Optagelsessamtykke + Opkaldes optages måske + Optagelse + Fjernede samtalen %1$s fra favoritter + Samtalen %1$s blev omdøbt + Gensend + Nulstil status + Det er ikke muligt at deltage i andre rum, mens du er i et opkald + Gem + Skan QR-kode + Synkroniser kun til betroede servere + Forbundet + Kun synlig for personer i denne instans og gæster + Lokal + Kun synlig for personer, der matches via telefonnummerintegration via Snak på mobil + Privat + Synkroniser til betroede servere og den globale og offentlige adressebog + Udgivet + Scope skift + Skift privatlivsniveau for %1$s + Rul til bunden + Søgeikon + få sekunder siden + Markeret + Send e-mail + Send til + Du har ikke tilladelse til at dele indhold til dette opkald + Send til ... + Send uden notifikation + Sæt + Angiv avatar fra kamera + Sæt status + Sæt statusbesked + Del + Deltag i samtalen %1$s den %2$s + Audio + Fil + Medier + Andet + Afstemning + Opkaldssamtale + Telefonsvarer + Vis blokeringsgrund + Vis blokerede deltagere + Favorit + Du har ikke tilladelse til at starte et opkald + startede et opkald + Statusbesked + Status omvendt + Skift til breakout rum + Skift til hovedrum + Tag et billede + Fejl under fotografering + At tage et billede er ikke muligt uden rettigheder + Tag foto igen + Send + Skift kamera + Beskær foto + Reducer billedstørrelse + Skift lygte + 30 minutter + Denne uge + Dette er en testbesked + Denne weekend + I dag + I morgen + Oversæt + Oversættelse + Kopier oversat tekst + Detekter sprog + Enhedsindstillinger + Kunne ikke finde sprog + Oversættelse fejlede + Fra + Til + og 1 anden indtaster … + indtaster ... + indtaster .. + og %1$s andre indtaster … + Fjern samtale fra arkiv + Når en samtale er genaktiveret, så vil den som standard vises igen. + Genaktiver %1$s + Fjern blokering + Ulæst + Upload ny avatar fra apparat + %1$s er ikke på kontoret og svarer måske ikke + %1$s er ikke på kontoret i dag + Erstatning: + Bruger avatar + Adresse + Fulde navn + E-mail + Telefonnummer + Twitter + Hjemmeside + Status + Kunne ikke hente personlig brugerinformation. + Personlig information ikke konfigureret + Tilføj navn, billede og kontakt information på din profilside. + Video opkald + Hvad er din status + + Se %d lignende besked + Se %d lignende beskeder + + + %d stemme + %d stemmer + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..b9a3586 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,727 @@ + + + Bearbeiten + Hinzufügen + Zu Notizen hinzufügen + Unterhaltung %1$s zu Favoriten hinzugefügt + Suche in %s + Offline erscheinen + Unterhaltung archivieren + Sobald eine Unterhaltung archiviert ist, wird sie standardmäßig ausgeblendet. Wählen Sie den Filter \"Archiviert\", um archivierte Unterhaltungen anzuzeigen. Direkte Erwähnungen werden weiterhin empfangen. + Archiviert + %1$s archiviert + Audioanruf + Bluetooth + Audio-Ausgabe + Telefon + Lautsprecher + Kabelgebundener Kopfhörer + Ihr Status wurde automatisch gesetzt + Avatar + Abwesend + Zurück-Schaltfläche + Sperren + Teilnehmer sperren + Sperrliste + Beschäftigt + Kalender + Erweiterte Anrufoptionen + Der Anruf läuft seit einer Stunde. + Anruf ohne Benachrichtigung + Kameraerlaubnis erteilt. Bitte wählen Sie die Kamera erneut. + Anmelden abbrechen + Avatar aus der Cloud auswählen + Statusmeldung löschen + Statusmeldung löschen nach + Schließen + Schließen-Symbol + Verbindung hergestellt + Keine Verbindung zum Server + Verbindung verloren - Gesendete Nachrichten werden der Warteschlange hinzugefügt + Aufnahme sperren für kontinuierliche Aufzeichnung der Sprachnachricht + Unterhaltung ist archiviert + Unterhaltung ist schreibgeschützt + Unterhaltung konnte nicht auf schreibgeschützt gesetzt werden + Unterhaltungen + Unterhaltung erstellen + Fehler melden + Benutzerdefiniert + Gefahrenzone + %1$s in %2$s + Avatar löschen + Sprachaufnahme löschen + Unterhaltung %1$s gelöscht + Nicht stören + Nicht löschen + Bearbeiten + Nachrichten, die älter als 24 Stunden sind, können nicht bearbeitet werden + Nachricht bearbeiten + Neueste + Verschlüsselt + Anruf beenden + Anruf für alle beenden + Beim Laden Ihrer Chats ist ein Problem aufgetreten + Fehler beim Entsperren des Teilnehmers + %1$s konnte nicht gespeichert werden + 15 Minuten + Ordner + Lade … + %1$s (%2$d) + Nachverfolgte Themen + 4 Stunden + Ausstehende Einladungen konnten nicht abgerufen werden + (Bearbeitet) + Interne Notiz + Unsichtbar + Sprachen konnten nicht abgerufen werden + Abrufen fehlgeschlagen + Später heute + Anruf verlassen + Sie haben die Unterhaltung %1$s verlassen + Weitere Ergebnisse laden + Ortszeit: %1$s + Standort-Berechtigung abgelehnt + Dies bitte in in App-Einstellungen aktivieren + Standortdienste deaktiviert + Bitte die Standortdienste (GPS) aktivieren, um diese Funktion zu nutzen + Unterhaltung sperren + Schloss-Symbol + Hand herunternehmen + Unterhaltung %1$s als gelesen markiert + Unterhaltung %1$s als ungelesen markiert + Erwähnt + Neue zuerst + Älteste zuerst + A - Z + Z - A + Größste zuerst + Kleinste zuerst + Nachricht kopiert + Möchten Sie diese Nachricht löschen? + Nachricht von Ihnen gelöscht + Bearbeitet von %1$s + Tippen, um die Umfrage zu öffnen + Keine Suchergebnisse + Beginnen Sie mit der Eingabe, um zu suchen … + Suche … + Nachrichten + Alle Benachrichtigungen stummschalten + Das ausgewählte Konto ist nun importiert und verfügbar + Über + Aktiver Benutzer + Konto hinzufügen + Das Konto ist zur Löschung vorgesehen und kann daher nicht verändert werden + Hauptmenü öffnen + Anhang hinzufügen + Emojis hinzufügen + Zur Unterhaltung hinzufügen + Teilnehmer hinzufügen + Zu Favoriten hinzufügen + OK, alles erledigt! + PIN: %1$s + %1$s entsperren + Um Bluetooth-Lautsprecher zu aktivieren, bitte die Berechtigung \"Geräte in der Nähe\" erteilen. + Als Videoanruf annehmen + Nur als Sprachanruf annehmen + Audioausgabe ändern + Kamera umschalten + Auflegen + Mikrofon umschalten + Bild-in-Bild-Modus öffnen + Wechseln Sie zum Eigenen Video + EINGEHEND + Name der Unterhaltung + Anrufbenachrichtigungen + %1$s hat die Hand gehoben + Verbinde erneut … + KLINGELT + %1$s ist in einem Gespräch + %1$s mit Telefon + %1$s mit Video + Keine Antwort nach 45 Sekunden, klicken Sie um es erneut zu versuchen + %s Anruf + %s Videoanruf + %s Sprachanruf + Um Videokommunikation zu ermöglichen, bitte die Berechtigung \"Kamera\" erteilen. + Abbrechen + Fähigkeiten konnten nicht abgefragt werden. Abbruch + Untertitel + Soll dem bisher unbekannten SSL-Zertifikat, ausgestellt von %1$s für %2$s, gültig von %3$s bis %4$s, vertraut werden? + Überprüfen Sie das Zertifikat + Ihre SSL-Konfiguration hat die Verbindung verhindert + Authentifizierungs-Zertifikat ändern + Passwort ändern + Bearbeitung abbrechen + Bearbeitung abbrechen + Alle Nachrichten löschen + Alle Nachrichten wurden gelöscht + Möchten Sie wirklich alle Nachrichten dieser Unterhaltung löschen? + Client-Zertifikat ändern + Client-Zertifikat einrichten + und + Kopieren + In die Zwischenablage kopiert + Erstellen + Deaktiviert + Ablehnen + Leider ist etwas schiefgelaufen! + Weitere Optionen + Setzen + Überspringen + Unbekannt + Authentifizierungs-Zertifikat auswählen + Verbinde … + Fertig + Beschreibung der Unterhaltung + Unterhaltungs-Information + Videoanruf + Sprachanruf + Unterhaltung nicht gefunden + Unterhaltungseinstellungen + Treten Sie einer Unterhaltung bei oder starten Sie eine neue + Begrüßen Sie Ihre Freunde und Kollegen! + Kopieren + Neue Unterhaltung erstellen + Umfrage erstellen + Sie: + Heute + Gestern + Löschen + Alle löschen + Unterhaltung löschen + Wenn Sie diese Unterhaltung löschen, dann wird diese auch für alle anderen Teilnehmer gelöscht. + Nachricht löschen + Nachricht gelöscht, sie wurde aber möglicherweise an andere Dienste weitergegeben. + Jetzt löschen + Benutzer %1$s wurde entfernt + Moderator absetzen + Sprachnachricht aufnehmen + Nachricht senden + Aktuelles Konto + Server + Server-Benachrichtigungs-App installiert? + Benutzer + Benutzerstatus aktiviert? + Android-Version + App + App-Name + Registrierte Benutzer + App-Version + Batterieoptimierung wird ignoriert, alles in Ordnung + Die Batterieoptimierung ist aktiviert und könnte Probleme verursachen. Dies sollte geändert werden! + Batterie-Einstellungen + Gerät + Checkliste zur Fehlerbehebung öffnen + Diagnoseseite öffnen + dontkillmyapp.com öffnen + Neuester Firebase-Push-Token-Abruf + Neueste Firebase-Push-Token-Generation + Kein Firebase-Push-Token festgelegt. Bitte erstellen Sie einen Fehlerbericht. + Firebase-Push-Token + Google Play-Dienste sind nicht verfügbar. Benachrichtigungen werden nicht unterstützt + Google Play-Dienste + Google Play-Dienste sind verfügbar + Aktuelle Push-Registrierung beim Push-Proxy + Noch nicht beim Push-Proxy registriert + Aktuelle Push-Registrierung am Server + Noch nicht am Server registriert + Meta-Information + Erzeugung des Systemberichts + Anrufbenachrichtigungskanal aktiviert? + Nachrichtenbenachrichtigungskanal aktiviert? + Benachrichtigungsberechtigungen + Telefon + Serverversion von Talk + Serverversion + Extern + Intern + Signalisierungsmodus + Ungültiges Passwort + Server befindet sich im Wartungsmodus. + App ist veraltet + Die App ist zu alt und wird von diesem Server nicht mehr unterstützt. Bitte aktualisieren. + Aktualisieren + Möchten Sie das Konto erneut autorisieren oder löschen? + Wenn Sie dieses Medium im Speicher sichern, können alle anderen Apps auf Ihrem Gerät darauf zugreifen. + Fortsetzen? + Nein + Im Speicher speichern? + Ja + Anzeigename konnte nicht abgerufen werden. Breche ab. + Der Anzeigename konnte nicht gespeichert werden. Breche ab. + Bearbeiten + Bearbeiten + Nachricht bearbeiten + Bearbeitet von der Administration + Menü für Veranstaltungsunterhaltungen + Planen + 8 Stunden + 4 Wochen + Aus + 1 Tag + 1 Stunde + 1 Woche + Chatnachrichten ablaufen lassen + Chatnachrichten können nach einer bestimmten Zeit gelöscht werden. Hinweis: Im Chat freigegebene Dateien werden für den Besitzer nicht gelöscht, aber sie werden nicht mehr in der Unterhaltung freigegeben. + Benachrichtigungseinstellungen konnten nicht abgerufen werden + Annehmen + Ablehnen + von %1$s um %2$s + Keine ausstehenden Einladungen + Sie haben ausstehende Einladungen + Zurück + Erlaubnis für Dateizugriff ist erforderlich + Unterhaltungen filtern + Benutzer kommt von einem öffentlichen Link + Sie: %1$s + Weiterleiten + Weiterleiten an … + Galerie + Sie haben noch keinen Server?\nKlicken Sie hier, um Anbieter zu finden + Zum Programmcode + Gruppe + Gast + Gastzugriff + Gastzugriff kann nicht aktiviert/deaktiviert werden. + Gästen erlauben, einen öffentlichen Link zu teilen, um an dieser Unterhaltung teilzunehmen. + Gäste zulassen + Passwort eingeben + Passwort für den Gastzugang + Fehler beim Setzen/Deaktivieren des Passworts. + Passwort setzen, um den öffentlichen Zugriff einzuschränken. + Passwortschutz + Einladungen erneut versenden + Einladungen wurden aufgrund eines Fehlers nicht gesendet. + Es wurden erneut Einladungen verschickt. + Unterhaltungs-Link teilen + Eine Nachricht eingeben … + Die Batterieoptimierung ist aktiviert. Dies sollte geändert werden, um sicherzustellen, dass Benachrichtigungen im Hintergrund funktionieren! Bitte klicken Sie auf OK und wählen Sie \"Alle Apps\" -> %1$s -> Nicht optimieren + Batterieoptimierung ignorieren + Wichtige Unterhaltung + \"Nicht stören\"-Benutzerstatus wird für wichtige Unterhaltungen ignoriert + Ungültige Zeit + Einladungen + Offenen Unterhaltungen beitreten + Behalten + Sie müssen einen neuen Moderator bestimmen, bevor Sie die Unterhaltung verlassen können. + %1$s | Zuletzt geändert: %2$s + Unterhaltung verlassen + Verlasse Anruf … + GNU General Public Lizenz, Version 3 + Lizenz + %s-Zeichen-Limit wurde erreicht + Lobby + Dieses Treffen ist geplant für %1$s + Das Treffen beginnt bald + Sie warten aktuell in der Lobby. + Ihre aktuelle Position + Standort-Berechtigung wird benötigt + Position unbekannt + Gesperrt + Zum Entsperren antippen + Nicht eingestellt + Als gelesen markieren + Als ungelesen markieren + Unterhaltung als wichtig markiert + Unterhaltung nicht als sensibel markiert + Unterhaltung als sensibel markiert + Unterhaltung nicht als wichtig markiert + Meeting beendet + Nachricht zu den Notizen hinzugefügt + Fehlgeschlagen + Fehler beim Senden der Nachricht: + Offline + Antwort abbrechen + Nachricht gelesen + Sende + Nachricht gesendet + Das Mikrofon ist aktiviert und Audio wird aufgenommen + Um Sprachkommunikation zu ermöglichen, bitte die Berechtigung \"Mikrofon\" erteilen. + Sie haben einen Anruf von %s verpasst + Moderator + Neue Unterhaltung + Sichtbarkeit + Ungelesene Erwähnungen + Ungelesene Nachrichten + %1$s nicht verfügbar (nicht installiert oder von der Administration eingeschränkt) + Gast + Nein + Keine offenen Unterhaltungen + Keine offenen Unterhaltungen, denen Sie beitreten können.\nEntweder existieren keine offenen Unterhaltungen oder Sie sind bereits allen offenen Unterhaltungen beigetreten. + Kein Proxy + Sie dürfen Audio nicht aktivieren! + Sie dürfen Video nicht aktivieren! + Nicht jetzt + %1$s auf %2$s Benachrichtigungskanal + Anrufe + Bei eingehenden Anrufen benachrichtigen + Nachrichten + Bei eingehenden Nachrichten benachrichtigen + Uploads + Über den Hochlade-Fortschritt benachrichtigen + Benachrichtigungseinstellungen + Benachrichtigungen sind nicht korrekt eingerichtet + Benachrichtigungsberechtigung und Akkueinstellungen sind korrekt eingerichtet, um Benachrichtigungen zu erhalten. Sollten Sie trotzdem Probleme mit den Benachrichtigungen haben, prüfen Sie bitte, ob die Benachrichtigungskanäle für Anrufe und Nachrichten aktiviert sind. Weitere Hilfe finden Sie unter DontKillMyApp.com oder in der Checkliste zur Fehlerbehebung. Wenn dies nicht hilft, gehen Sie bitte zur Diagnoseseite und senden Sie einen Fehlerbericht. + Fehlerbehebung bei Benachrichtigungen + Immer benachrichtigen + Bei Erwähnung benachrichtigen + Nie benachrichtigen + Aktuell offline, bitte die Verbindung prüfen + OK + Laufendes Meeting + Unterhaltung für registrierte Benutzer öffnen + Auch für Gast-App-Benutzer öffnen + Besitzer + Teilnehmer + Teilnehmer hinzufügen + Passwort + Berechtigungen festlegen + Einige Berechtigungen wurden abgelehnt. + Bitte alle Berechtigungen erteilen. + Einstellungen öffnen + Bitte Berechtigungen unter Einstellungen > Berechtigungen erteilen + Konto nicht gefunden + Chatten über %s + Mikrofon stummschalten + Mikrofon aktivieren + Nachrichten + Datenschutz + Persönliche Informationen + Zum Moderator ernennen + Öffentliche Unterhaltung + Push-Benachrichtigungen sind deaktiviert + Leider ist etwas schief gelaufen, der Fehler ist %1$s + Leider ist etwas schief gelaufen, die Test-Push-Nachricht kann nicht abgerufen werden + Die Push-Benachrichtigung wurde gesendet. Sie sollten nun auf diesem Gerät eine Benachrichtigung mit dem Titel „Testen von Push-Benachrichtigungen“ erhalten. + Funkgerät-Modus + Bei deaktiviertem Mikrofon &drücken. Halten Sie zum Senden die Sprechtaste gedrückt (Funkgerät-Modus) + Erinnere mich später + Von Favoriten entfernen + Gruppe und Mitglieder entfernen + Teilnehmer entfernen + Passwort entfernen + Team und Mitglieder entfernen + Unterhaltung umbenennen + Umbenennen + Antworten + Privat antworten + Raum wird beibehalten + Speichern + Gespeichert + 30 Sekunden + 5 Minuten + 1 Minute + 10 Minuten + Unmittelbar + 600 + 60 + 30 + 300 + Suchen + Suche löschen + Konto auswählen + Nachricht aktualisieren + Sprachaufnahme senden + Sensible Unterhaltung + Die Nachrichtenvorschau wird in der Unterhaltungsliste und den Benachrichtigungen deaktiviert + %1$s hat ein GIF gesendet. + Sie haben ein GIF gesendet. + %1$s hat ein Video gesendet. + Sie haben ein Video gesendet. + %1$s hat eine Audio-Datei gesendet. + Sie haben eine Audio-Datei gesendet. + %1$s hat ein Bild gesendet. + Sie haben ein Bild gesendet. + %1$s hat eine Deck-Karte gesendet + Prüfe Verbindung zum Server + Bitte aktualisieren Sie Ihre %1$s Datenbank + Das ausgewählte Konto konnte nicht importiert werden + Der Link zu Ihrer %1$s Webseite, wenn Sie diese im Browser öffnen. + Ein Konto aus der App %1$s importieren + Konto importieren + Konten aus der App %1$s importieren + Konten importieren + Bitte den Wartungsmodus von %1$s beenden + Bitte beenden Sie Ihre %1$s Installation + Teste Verbindung + Auf dem Server ist keine unterstützte Talk-App installiert + Serveradresse https://… + %1$s funktioniert nur mit %2$s 13 und höher + Neues Passwort setzen + Passwort festlegen + Einstellungen + Statt ein neues Konto hinzuzufügen, wurde Ihr aktuelles Konto aktualisiert. + Erweitert + Aussehen + Anrufe + Bitte kontaktieren Sie die Administration von + Die Diagnoseseite öffnen, um die Einstellungen zu überprüfen oder einen Fehlerbericht zu erstellen + Diagnose + Weist die Tastatur an, das personalisierte Lernen zu deaktivieren (ohne Garantien) + Inkognito-Tastatur + Kein Ton + Die Talk-App ist nicht auf dem Server installiert, auf dem Sie versucht haben sich zu authentisieren + Benachrichtigungen + Benachrichtigungen werden abgelehnt + Benachrichtigungen werden gewährt + Nachrichten + Abgleich von Kontakten basierend auf der Telefonnummer zur Integration der Talk-Verknüpfung im Telefonbuch + Fehler 429: Zu viele Anfragen + Sie können Ihre Telefonnummer so einstellen, dass andere Benutzer Sie finden können. + Telefonnummer eingeben + Ungültige Telefonnummer + Telefonnummer festgelegt + Telefonnummer + Rufnummernintegration + Datenschutz + Proxy-Host + Proxy-Passwort + Proxy-Port + Proxy-Typ + Proxy-Benutzername + Ihren Lesestatus teilen und den anderer anzeigen + Lesestatus + Konto erneut autorisieren + Entfernen + Konto entfernen + Bitte bestätigen Sie, dass Sie dieses Konto entfernen möchten. + %1$s sperren mit der Android-Bildschirmsperre oder einer unterstützten biometrischen Methode + Zeitüberschreitung der Bildschirm-Inaktivitätsperre + Bildschirmsperre + Verhindert Screenshots in der aktuellen Liste und in der App + Bildschirmsicherheit + Die Serverversion ist sehr alt und wird ab dem nächsten Update nicht mehr unterstützt + Die Serverversion ist zu alt und wird von dieser Version der Android-App nicht unterstützt + Nicht unterstützter Server + Server-Benachrichtigungs-App ist nicht installiert + Durch Energiesparmodus gesetzt + Dunkel + Systemstandard verwenden + Design + Hell + Design + Ihren Schreibstatus teilen und den anderer anzeigen + Der Eingabestatus ist nur verfügbar, wenn ein Hochleistungs-Backend (HPB) verwendet wird. + Schreibstatus + Proxy erfordert eine Authentifizierung + Warnung + Nur das aktuelle Konto kann reautorisiert werden + Kontakt teilen + Leseberechtigung für Kontakte erforderlich + Aktuelle Position teilen + Link teilen + Ort teilen + Diesen Ort teilen + Konto auswählen + Geteilte Elemente + Deck-Karte + Bilder, Dateien, Sprachnachrichten … + Keine geteilten Elemente + Ort + Geteilter Ort + Wenn Benachrichtigungen nicht korrekt konfiguriert sind, zeige regelmäßig eine Warnung + Zeige regelmäßige Benachrichtigungswarnung + Sortieren nach + Gruppenchat starten + Startzeit + Konto wechseln + Team + Push-Benachrichtigungen testen + Testergebnisse + Heute um %1$s + Morgen um %1$s + Dateien auswählen + Diese Dateien an %1$s senden? + Diese Datei an %1$s senden? + Hochladen leider fehlgeschlagen + %1$s konnte nicht hochgeladen werden + Fehler + Freigabe von %1$s + Von Gerät hochladen + Lade hoch + %1$s zu %2$s - %3$s\%% + Foto aufnehmen + Video aufnehmen + Benutzer + Videoaufnahme von %1$s + Talk-Aufnahme von %1$s (%2$s) + Halten zum Aufnehmen, zum Versenden loslassen. + Erlaubnis für Audioaufnahmen ist erforderlich + « Schieben zum Abbrechen + Online-Seminar + Ja + Nächste Woche + Keine archivierten Unterhaltungen + Keine Offlinenachrichten gespeichert + Keine Rufnummernintegration aufgrund fehlender Berechtigungen + Alle Nachrichten + Nur @-Erwähnungen + Aus + Standard + Unterhaltungseinstellungen folgen + 1 Stunde + Online + Online-Status + Offene Unterhaltungen + In Dateien-App öffnen + Notizen öffnen + Zu Thema gehen + Sprachnachricht wiedergeben/pausieren + Steuerung der Wiedergabegeschwindigkeit + Option hinzufügen + Abstimmung bearbeiten + Umfrage beenden + Soll diese Umfrage wirklich beendet werden? Das kann nicht rückgängig gemacht werden. + Sie können nicht mit mehreren Optionen für diese Umfrage abstimmen. + Mehrere Antworten + Option %1$d löschen + Option %1$d + Optionen + Private Umfrage + Frage + Ihre Frage + Ergebnisse + Einstellungen + Abstimmen + Stimme abgegeben + Zuvor eingestellt + Der QR-Code konnte nicht gelesen werden + Hand heben + Alle + Dateien können nicht ohne Berechtigung geteilt werden + Neueste Themen + Der Anruf wird aufgezeichnet + Aufnahmestart abbrechen + Aufnahme fehlgeschlagen. Bitte wenden Sie sich an Ihre Administration. + Aufnahme beginnen + Möchten Sie die Aufnahme wirklich beenden? + Anrufaufnahme beenden + Aufnahme stoppen + Stoppe Aufnahme … + Für alle Unterhaltungen ist eine Zustimmung zur Aufzeichnung erforderlich + Die Aufnahme kann Ihre Stimme sowie Video von der Kamera und einer Bildschirmfreigabe beinhalten. Vor der Teilnahme am Anruf ist Ihre Zustimmung hierzu erforderlich. Stimmen Sie zu? + Zur Teilnahme an dieser Unterhaltung ist die Zustimmung zur Aufzeichnung erforderlich. + Zustimmung zur Aufzeichnung + Die Unterhaltung wird möglicherweise aufgezeichnet. + Aufnahme + Unterhaltung %1$s aus Favoriten entfernt + Unterhaltung %1$s wurde umbenannt + Nochmals senden + Status zurücksetzen + Während einer Unterhaltung können Sie keinen anderen Räumen beitreten + Speichern + QR-Code scannen + Nur mit vertrauenswürdigen Servern synchronisieren + Federated + Nur für Personen dieser Instanz und Gäste sichtbar + Lokal + Nur sichtbar für Personen, die über die Rufnummernintegration von Talk auf Mobiltelefonen abgeglichen wurden. + Privat + Mit vertrauenswürdigen Servern und dem globalen sowie öffentlichen Adressbuch synchronisieren + Veröffentlicht + Bereich umschalten + Ändere Datenschutzstufe von %1$s + Nach unten blättern + Such-Symbol + Gerade eben + Ausgewählt + E-Mail senden + Senden an + Sie sind nicht berechtigt, Inhalte in diesem Chat zu teilen + Senden an … + Ohne Benachrichtigung senden + Setzen + Avatar mit der Kamera setzen + Status setzen + Statusnachricht setzen + Freigabe + Unterhaltung %1$s bei %2$s beitreten + Audio + Datei + Medien + Sonstiges + Umfrage + Anrufaufnahmen + Sprachnachrichten + Sperrgrund anzeigen + Gesperrte Teilnehmer anzeigen + Favorit + Sie dürfen keinen Anruf zu tätigen + Ein Thema erstellen + hat einen Anruf begonnen + Statusnachricht + Status zurückgesetzt + Zu Gruppenraum wechseln + Zu Hauptraum wechseln + Ein Foto aufnehmen + Fehler beim Aufnehmen des Fotos + Das Fotografieren ist ohne Berechtigungen nicht möglich + Foto nochmals aufnehmen + Senden + Kamera wechseln + Foto beschneiden + Bildgröße verringern + Taschenlampe umschalten + 30 Minuten + Diese Woche + Dies ist eine Testnachricht + Dieses Wochenende + Themenerstellung abbrechen + Themen-Benachrichtigungen + Antwort + Thementitel + Keine Themen gefunden + Heute + Morgen + Übersetzen + Übersetzung + Übersetzten Text kopieren + Sprache erkennen + Geräteeinstellungen + Sprache konnte nicht erkannt werden + Übersetzung fehlgeschlagen + Von + An + und 1 anderer schreibt… + schreiben… + schreibt… + und %1$s andere schreiben… + Unterhaltung dearchivieren + Sobald eine Unterhaltung dearchiviert wurde, wird sie standardmäßig wieder angezeigt. + %1$s dearchiviert + Entsperren + Ungelesen + Neuen Avatar vom Gerät hochladen + %1$s ist nicht im Büro und antwortet möglicherweise nicht + %1$s ist heute nicht im Büro + Ersatz: + Benutzer-Avatar + Adresse + Vollständiger Name + E-Mail + Telefonnummer + X + Webseite + Status + Persönliche Nutzerinformationen konnten nicht geladen werden + Keine persönlichen Informationen eingestellt + Fügen Sie Name, Bild und Kontaktdaten auf Ihrer Profilseite hinzu. + Videoanruf + Wie ist Ihr Status? + + %d ähnliche Nachricht ansehen + %d ähnliche Nachrichten ansehen + + + Diese Unterhaltung wird für alle bei Inaktivität in %1$d Tag gelöscht + Diese Unterhaltung wird für alle bei Inaktivität in %1$d Tagen gelöscht + + + %d Antwort + %d Antworten + + + %d Stimme + %d Stimmen + + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 0000000..1bb70d8 --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,425 @@ + + + Επεξεργασία + Προσθήκη + Αναζήτηση στο %s + Αρχειοθετήθηκε + Έξοδος ήχου + Τηλέφωνο + Μεγάφωνο + Εικόνα προφίλ + Λείπω + Απασχολημένος + Ημερολόγιο + Επιλογή εικόνας προφίλ από το cloud + Εκκαθάριση μηνύματος κατάστασης + Εκκαθάριση μηνύματος κατάστασης μετά από + Κλείσιμο + Εδραίωση σύνδεσης + Συνομιλίες + Δημιουργία συνομιλίας + Προσαρμοσμένο + Επικίνδυνη ζώνη + Διαγραφή εικόνας προφίλ + Μην ενοχλείτε + Να μη γίνεται εκκαθάριση + Επεξεργασία + Επεξεργασία μηνύματος + Πρόσφατα + Κρυπτογραφημένο + Τερματισμός κλήσης + Αποτυχία αποθήκευσης %1$s + 15 λεπτά + φάκελος + Φόρτωση … + %1$s (%2$d) + 4 ώρες + Αόρατο + Αργότερα σήμερα + Αποχώρηση από την κλήση + Φόρτωση περισσοτέρων αποτελεσμάτων + Κλειδώστε τη συνομιλία + Κατεβάστε το χέρι + Νεότερο πρώτα + Παλαιότερο πρώτα + Α - Ω + Ω - Α + Μεγαλύτερο πρώτα + Μικρότερο πρώτα + Το μήνυμα διαγράφηκε από εσάς + Κανένα αποτέλεσμα + Μηνύματα + Ο επιλεγμένος λογαριασμός έχει εισαχθεί και είναι τώρα διαθέσιμος + Περί + Ενεργός χρήστης + Προσθήκη λογαριασμού + Ο λογαριασμός προγραμματίστηκε να διαγραφή και δεν μπορεί να αλλάξει + Άνοιγμα κυρίως μενού + Προσθήκη συνημμένου + Προσθήκη emojis + Προσθήκη στη συζήτηση + Προσθήκη συμμετεχόντων + Προσθήκη στα αγαπημένα + Εντάξει, ολοκληρώθηκαν όλα! + Ξεκλείδωμα %1$s + Κλείσιμο τηλεφώνου + ΕΙΣΕΡΧΟΜΕΝΗ + Όνομα συνομιλίας + Επανασύνδεση ... + ΚΛΗΣΗ + %1$s σε κλήση + %1$s με τηλέφωνο + %1$s με βίντεο + Πέρασαν 45 δεύτερα, δοκιμάστε ξανά + %s κλήση + %s βίντεο κλήση + %s κλήση με ήχο + Ακύρωση + Η εμφάνιση δυνατοτήτων απέτυχε, ματαιωση + Eμπιστεύεσαι το άγνωστο έως τώρα πιστοποιητικό SSL που εκδόθηκε %1$s για %2$s, έγκυρο από %3$s σε%4$s; + Ελέγξτε το πιστοποιητικό + Η ρύθμιση SSL απέτρεψε την σύνδεση + Αλλαγή πιστοποιητικού πιστοποίησης + Αλλαγή συνθηματικού + Ακύρωση επεξεργασίας + Ακύρωση επεξεργασίας + Διαγραφή όλων των μηνυμάτων + Όλα τα μηνύματα διαγράφηκαν + Θέλετε πραγματικά να διαγράψετε όλα τα μηνύματα σε αυτήν τη συνομιλία; + Αλλαγή πιστοποιητικού πελάτη + Ρύθμιση πιστοποιητικού πελάτη + και + Αντιγραφή + Αντιγράφηκε στο πρόχειρο + Δημιουργία + Απενεργοποιημένο + Τους ζυγούς λύσατε! + Συγνώμη, κάτι πήγε στραβά! + Περισσότερες επιλογές + Ορισμός + Παράλειψη + Άγνωστο + Επιλογή πιστοποιητικού πιστοποίησης + Σύνδεση … + Ολοκληρώθηκε + Πληροφορίες συνομιλίας + Κλήση βίντεο + Κλήση ήχου + Η συζήτηση δεν βρέθηκε + Ρυθμίσεις συνομιλίας + Συμμετέχετε σε συνομιλία ή ξεκινήστε μια νέα + Πείτε γειά σε φίλους και συνεργάτες! + Αντιγραφή + Δημιουργία νέας συνομιλίας + Δημιουργία ψηφοφορίας + Σήμερα + Χθές + Διαγραφή + Διαγραφή όλων + Διαγραφή συνομιλίας + Αν διαγράψετε την συνομιλία, θα διαγραφεί επίσης για όλους τους υπόλοιπους συμμετέχοντες. + Διαγραφή μηνύματος + Το μήνυμα διαγράφηκε με επιτυχία, αλλά ενδέχεται να έχει διαρρεύσει σε άλλες υπηρεσίες + Υποβάθμιση από συντονιστή + Εγγραφή φωνητικού μηνύματος + Αποστολή μηνύματος + Τρέχον λογαριασμός + Διακομιστής + Χρήστης + Έκδοση Android + Εφαρμογή + Όνομα εφαρμογής + Ρυθμίσεις μπαταρίας + Συσκευή + Τηλέφωνο + Εξωτερικό + Εσωτερικό + Λανθασμένο συνθηματικό + Ενημέρωση + Θέλετε να εξουσιοδοτήσετε εκ νέου ή να διαγράψετε αυτόν τον λογαριασμό; + Όχι + Ναι + Το εμφανιζόμενο όνομα δεν ήταν δυνατό να ληφθεί, ματαίωση + Το εμφανιζόμενο όνομα δεν μπορεί να αποθηκευτεί, ματαίωση + Επεξεργασία + Επεξεργασία + Επεξεργασία μηνύματος + 8 ώρες + Απενεργοποίηση + 1 μέρα + 1 ώρα + 1 εβδομάδα + Αποτυχία λήψης ρυθμίσεων σήματος + Αποδοχή + Απόρριψη + Πίσω + Χρήστης από δημόσιο σύνδεσμο + Εσείς: %1$s + Προώθηση + Προώθηση σε … + Εκθεσιακός χώρος + Δεν έχετε ακόμα διακομιστή;\nΚάντε κλικ εδώ για να αποκτήσετε έναν από κάποιον πάροχο + Λήψη πηγαίου κώδικα + Ομάδα + Επισκέπτης + Επιτρέψτε τους επισκέπτες + Εισάγετε συνθηματικό + Ορίστε έναν κωδικό πρόσβασης για να περιορίσετε ποιος μπορεί να χρησιμοποιήσει τον δημόσιο σύνδεσμο. + Προστασία συνθηματικού + Επαναποστολή προσκλήσεων + Κοινή χρήση συνδέσμου συνομιλίας + Εισάγετε ένα μήνυμα ... + Σημαντική συνομιλία + Προσκλήσεις + Δημιουργία νέας συνομιλίας + %1$s Τελευταία τροποποίηση %2$s + Εγκατάλειψη συνομιλίας + Αποχώρηση από την κλήση ... + Γενική Άδεια Δημόσιας Χρήσης GNU, έκδοση 3 + Άδεια χρήσης + Το όριο %s χαρακτήρων έχει συμπληρωθεί + Αναμονή + Αυτή τη στιγμή είστε σε αναμονή + Η τρέχουσα τοποθεσία σας + απαιτείται δικαιώματα τοποθεσίας + Άγνωστη θέση + Κλειδώθηκε + Αγγίξτε για ξεκλείδωμα + Δεν ορίστηκε + Σήμανση ως αναγνωσμένο + επισήμανση ως μή-αναγνωσμένο + Απέτυχε + Ακύρωση απάντησης + Το μήνυμα διαβάστηκε + Το μήνυμα στάλθηκε + Συντονιστής + Νέα συνομιλία + Ορατότητα + Μη αναγνωσμένα μηνύματα + Το %1$s δεν είναι διαθέσιμο (δεν έχει εγκατασταθεί ή έχει απαγορευθεί από τον διαχειριστή) + Επισκέπτης + Όχι + Κανένας διαμεσολαβητής + Όχι τώρα + %1$s από %2$s κανάλι ειδοποίησης + Kλήσεις + Ειδοποίηση για εισερχόμενες κλήσεις + Μηνύματα + Μεταφορτώσεις + Ρυθμίσεις ειδοποιήσεων + Πάντα ειδοποίηση + Ειδοποίηση όταν αναφέρεται + Χωρίς ειδοποίηση + Σε αποσύνδεση, παρακαλώ ελέγξτε την σύνδεσή σας + OK + Άνοιγμα συνομιλίας στους εγγεγραμμένους χρήστες + Ανοιχτό επίσης για επισκέπτες της εφαρμογής + Ιδιοκτήτης + Συμμετέχοντες + Προσθήκη συμμετεχόντων + Συνθηματικό + Άνοιγμα ρυθμίσεων + Δεν βρέθηκε λογαριασμός + Συνομιλία μέσω %s + Μηνύματα + Ιδιωτικότητα + Προσωπικές πληροφορίες + Προαγωγή από συντονιστή + Οι ειδοποιήσεις push απενεργοποιήθηκαν + Push-to-talk + Με απενεργοποιημένο το μικρόφωνο, πιέστε και κρατήστε το & για χρήση του Push-to-talk + Θύμισέ μου αργότερα + Αφαίρεση από τα αγαπημένα + Αφαίρεση ομάδων και μελών + Αφαίρεση συμμετέχοντα + Μετονομασία συνομιλίας + Μετονομασία + Απάντηση + Απάντηση ιδιωτικά + Αποθήκευση + Επιτυχής αποθήκευση + 30 δευτερόλεπτα + 5 λεπτά + 1 λεπτό + 10 λεπτά + 600 + 60 + 30 + 300 + Αναζήτηση + Εκκαθάριση αναζήτησης + Επιλογή λογαριασμού + %1$s έστειλε GIF. + Στείλατε εικόνα GIF. + %1$s έστειλε βίντεο. + Στείλατε βίντεο. + %1$s έστειλε  ήχο. + Στείλατε αρχείο ήχου. + %1$s έστειλε εικόνα. + Στείλατε εικόνα. + Δοκιμή σύνδεσης με διακομιστή + Παρακαλούμε αναβαθμίστε την %1$s βάση δεδομένων σας + Αποτυχία εισαγωγής επιλεγμένου λογαριασμού + Αυτός είναι ο σύνδεσμος για τη διεπαφή του ιστότοπού σας %1$s όταν το ανοίγετε στο πρόγραμμα περιήγησης. + Εισαγωγή λογαριασμού από την %1$s εφαρμογή + Εισαγωγή λογαριασμού + Εισαγωγή λογαριασμών από την εφαρμογή %1$s + Εισαγωγή λογαριασμών + Παρακαλώ θέστε τον %1$sεκτος συντήρησης + Παρακαλούμε ολοκληρώστε την %1$s εγκατάσταση + Έλεγχος σύνδεσης + Ο διακομιστής δεν έχει εγκατεστημένη την υποστήριξη εφαρμογής Talk + Διεύθυνση διακομιστή https://… + %1$s δουλεύει μόνο με %2$s 13 και πάνω + Ορισμός νέου συνθηματικού + Ρυθμίσεις + Ο υπάρχων λογαριασμός ενημερώθηκε, χωρίς την εισαγωγή νέου + Για προχωρημένους + Εμφάνιση + Κλήσεις + Ενημερώνει το πληκτρολόγιο για να απενεργοποιήσει την προσωπική μάθηση (χωρίς εγγυήσεις) + Πληκτρολόγιο ανώνυμης περιήγησης + Χωρίς ήχο + Προσπαθείτε να πιστοποιήσετε ξανά την εφάρμογή Talk που δεν είναι εγκατεστημένη στον διακομιστή + Ειδοποιήσεις + Μηνύματα + Αντιστοιχίστε επαφές σύμφωνα με τον αριθμό τηλεφώνου για την ενσωμάτωση συντόμευσης Ομιλίας στο σύστημα επαφών της εφαρμογής + Μπορείτε να ορίσετε τον αριθμό τηλεφώνου σας, ώστε να μπορούν να σας βρίσκουν άλλοι χρήστες + Εισαγωγή τηλεφωνικού αριθμού + Μη έγκυρος αριθμός τηλεφώνου + Επιτυχής ορισμός αριθμού τηλεφώνου + Αριθμός τηλεφώνου + Ενσωμάτωση αριθού τηλεφώνου + Ιδιωτικότητα + Σύστημα διαμεσολαβητή + Συνθηματικό διαμεσολαβητή + Θύρα διαμεσολαβητή + Τύπος διαμεσολαβητή + Όνομα χρήστη διαμεσολαβητή + Διαμοιρασμός της κατάστασης-ανάγνωσης μου και εμφάνιση της κατάστασης-ανάγνωσης άλλων + Κατάσταση ανάγνωσης + Επαναπιστοποίηση λογαριασμού + Αφαίρεση + Αφαίρεση λογαριασμού + Παρακαλώ επιβεβαιώστε την πρόθεσή σας να αφαιρέσετε το λογαριασμό, + Κλείδωμα %1$sμε Android οθόνη κλειδώματος ή υποστηριζόμενη μέθοδο βιομετρικών + Χρονικό όριο αδράνειας κλειδώματος οθόνης + Κλείδωμα οθόνης + Αποτρέπει εικόνες στιγμιότυπων στην πρόσφατη λίστα και μέσα στην εφαρμογή + Ασφάλεια οθόνης + Η έκδοση διακομιστή είναι πολύ παλιά και δεν θα υποστηρίζεται στην επόμενη έκδοση! + Η έκδοση διακομιστή είναι πολύ παλιά και δεν υποστηρίζεται από αυτήν την έκδοση της εφαρμογής Android + Μη υποστηριζόμενος διακομιστής + Σκούρο + Χρησιμοποιήστε το προεπιλεγμένο σύστημα + θέμα + Φωτεινό + Θέμα + Ο διαμεσολαβητής απαιτεί διαπιστευτήρια + Προειδοποίηση + Μόνο ο παρόν λογαριασμός μπορεί να επαναεγκριθεί + Απαιτείτούνται δικαιώματα ανάγνωσης για τις επαφές + Διαμοιρασμός τρέχουσας τοποθεσίας + Διαμοιρασμός συνδέσμου + Διαμοιρασμός τοποθεσίας + Διαμοιρασμός αυτής της τοποθεσίας + Επιλογή λογαριασμού + Κάρτα του Deck + Τοποθεσία + Διαμοιρασμένες τοποθεσίες + Ταξινόμηση κατά + Ώρα έναρξης + Αλλαγή λογαριασμού + Επιλογή αρχείων + Να σταλούν αυτά τα αρχεία στον %1$s; + Να σταλεί αυτό το αρχείο στον %1$s; + Λυπούμαστε, η μεταφόρτωση απέτυχε + Διαμοιρασμός από %1$s + Μεταφόρτωση από συσκευή + Γίνεται μεταφόρτωση + Βγάλε φωτογραφία + Χρήστης + Κρατήστε για εγγραφή, αφήστε για αποστολή. + Απαιτούνται δικαιώματα για ηχογράφηση + « Σύρετε για ακύρωση + Webinar + Ναι + Επόμενη εβδομάδα + Δεν υπάρχει ενσωμάτωση αριθμού τηλεφώνου λόγω έλλειψης δικαιωμάτων + Απενεργοποιημένο + Προεπιλογή + 1 ώρα + Σε σύνδεση + Κατάσταση σε σύνδεση + Άνοιγμα συνομιλιών + Άνοιγμα την εφαρμογή Αρχεία + Αναπαραγωγή/παύση ηχητικού μηνύματος + Προσθήκη επιλογής + Επιλογές + Ιδιωτική δημοσκόπηση + Αποτελέσματα + Ρυθμίσεις + Ψήφος + Σηκώστε το χέρι + \'Ολα + Η κοινή χρήση αρχείων από τον χώρο αποθήκευσης δεν είναι δυνατή χωρίς δικαιώματα + Έναρξη εγγραφής + Καταγραφή + Αποθήκευση + Scan QR Code + Συγχρονισμός μόνο με έμπιστους διακομιστές. + Federated + Ορατό μόνο σε άτομα σε αυτήν την εγκατάσταση και σε επισκέπτες + Τοπικά + Ορατό μόνο σε άτομα που αντιστοιχίζονται μέσω ενσωμάτωσης αριθμού τηλεφώνου μέσω του Talk σε κινητά + Ιδιωτικά + Συγχρονισμός με έμπιστους διακομιστές και το παγκόσμιο και δημόσιο βιβλίο διευθύνσεων + Δημοσιεύτηκε + Αλλαγή επιπέδου ασφαλείας του %1$s + Μετακινηθείτε προς τα κάτω + δευτερόλεπτα πριν + Επιλέχθηκαν + Αποστολή email + Αποστολή σε + Αποστολή σε … + Ορισμός + Ορισμός κατάστασης + Ορισμός μηνύματος κατάστασης + Διαμοιρασμός + Ήχος + Αρχείο + Μέσα ενημέρωσης + Άλλο + Ομιλία + Αγαπημένο + Μήνυμα κατάστασης + Βγάλε μια φωτογραφία + Αποστολή + Εναλλαγή κάμερας + Εναλλαγή φακού + 30 λεπτά + Αυτή την εβδομάδα + Αυτό το Σαββατοκύριακο + Σήμερα + Αύριο + Μετάφραση + Ανίχνευση γλώσσας + Ρυθμίσεις συσκευής + Δεν ήταν δυνατός ο εντοπισμός της γλώσσας + Από + Έως + Μη αναγνωσμένο + Μεταφόρτωση νέας εικόνας προφίλ από την συσκευή + Άβαταρ χρήστη + Διεύθυνση + Πλήρες όνομα + Ηλ. ταχυδρομείο + Αριθμός τηλεφώνου + Twitter + Ιστοσελίδα + Κατάσταση + Αποτυχία ανάκτησης προσωπικών πληροφοριών χρήστη. + Δεν ορίστηκαν προσωπικές πληροφορίες + Προσθέστε όνομα, εικόνα και λεπτομέρειες επικοινωνίας στο προφίλ σας. + Ποια είναι η κατάστασή σας; + diff --git a/app/src/main/res/values-es-rEC/strings.xml b/app/src/main/res/values-es-rEC/strings.xml new file mode 100644 index 0000000..23c2e45 --- /dev/null +++ b/app/src/main/res/values-es-rEC/strings.xml @@ -0,0 +1,526 @@ + + + Editar + Guardar + Compartir en %s + Aparecer como desconectado + Archivado + Bluetooth + Salida de audio + Teléfono fijo + Altavoz + Auriculares con cable + Tu estado se estableció automáticamente + Avatar + Ausente + Ocupado + Calendario + Opciones avanzadas de llamada + Llamar sin notificación + Se ha concedido el permiso de la cámara. Por favor, elige la cámara de nuevo. + Elegir avatar desde la nube + Borrar mensaje de estado + Borrar mensaje de estado después de + Cerrar + Conexión establecida + Bloquear grabación para grabación continua del mensaje de voz. + Conversaciones + Crear conversación + Personalizado + Zona peligrosa + Borrar avatar + No molestar + No borrar + Editar + Editar mensaje + Reciente + Cifrado + Finalizar llamada para todos + Hubo un problema al cargar tus chats + Error al guardar %1$s + 15 minutos + carpeta + Loading … + %1$s (%2$d) + 4 horas + Invisible + Dejar la llamada + Cargar más resultados + Bloquear conversación + Símbolo de bloqueo + Bajar la mano + Mencionado + Más reciente primero + Más antiguo primero + A - Z + Z - A + Más grande primero + Más pequeño primero + Mensaje eliminado por ti + Toca para abrir la encuesta + No hay resultados de búsqueda + Comienza a escribir para buscar... + Buscar... + Mensajes + Silenciar todas las notificaciones + La cuenta seleccionada ha sido importada y está disponible + Acerca + Usuario activo + Agregar cuenta + La cuenta está calendarizada para ser borrada, y no puede ser modificada + Abrir menú principal + Agregar adjunto + Agregar emojis + Agregar a la conversación + Agregar participantes + Agregar a tus favoritos + ¡OK, ya terminamos! + Fijar: %1$s + Desbloquear %1$s + Responder como videollamada + Responder solo como llamada de voz + Cambiar salida de audio + Alternar cámara + Colgar + Alternar micrófono + Abrir modo de imagen en imagen + Cambiar a video propio + ENTRANTE + Nombre de la conversación + Notificaciones de llamadas + %1$s levantó la mano + Reconectando... + LLAMANDO + %1$s en llamada + %1$s con teléfono + %1$s con video + Sin respuesta en 45 segundos, toca para intentarlo de nuevo + %s llamada + %s videollamada + %s llamada de voz + Cancelar + Se presentó una falla al recuperar las capacidades, abortando + ¿Confias en el certificado SSL, hasta ahora desconocido, emitido por %1$s para %2$s, con validez del %3$s al %4$s? + Verifica el certificado + Tu configuración de SSL impidió la conexión + Cambiar el certificado de atuenticación + Cambiar contraseña + Cancelar edición + Cancelar edición + Borrar todos los mensajes + Todos los mensajes se han eliminado + ¿Realmente quieres borrar todos los mensajes de esta conversación? + Cambia el certificado del cliente + Configura un certificado de cliente + y + Copiar + Copiado al portapapeles + Crear + Deshabilitado + Descartar + ¡Lo sentimos, algo salió mal! + Más opciones + Establecer + Omitir + Desconocido + Seleccionar el certificado de autenticación + Conectando... + Terminado + Descripción de la conversación + Información de la conversación + Video llamada + Llamada de voz + Conversación no encontrada + Configuración de la conversación + Únete a la conversación o incia una nueva + ¡Saluda a tus amigos y colegas! + Copiar + Crear encuesta + Hoy + Ayer + Borrar + Borrar todo + Borrar conversación + Si borras la conversación, también se borrará para todos los demás participantes. + Eliminar mensaje + El mensaje se borró correctamente, pero podría haber sido filtrado a otros servicios + Degradar de moderador + Grabar mensaje de voz + Enviar mensaje + Cuenta actual + Servidor + Usuario + Versión de Android + Aplicación + Nombre de la aplicación + Usuarios registrados + Configuración de la batería + Dispositivo + Externo + Interna + Contraseña inválida + El servidor está actualmente en modo de mantenimiento. + La aplicación está desactualizada + La aplicación es demasiado antigua y ya no es compatible con este servidor. Por favor, actualízala. + Actualizar + ¿Quieres volver a autorizar o eliminar esta cuenta? + No se pudo obtener el nombre a desplegar, abortando + No se pudo almacenar el nombre a desplegar, abortando + Editar + Editar + Editar mensaje + 8 horas + 4 semanas + Apagado + 1 día + 1 hora + 1 semana + Vencimiento de mensajes de chat + Los mensajes de chat pueden vencer después de cierto tiempo. Nota: Los archivos compartidos en el chat no se eliminarán para el propietario, pero ya no se compartirán en la conversación. + Error al obtener la configuración de señalización + Aceptar + Rechazar + Atrás + Se requiere permiso para acceder a archivos + Usuario que sigue un enlace público + Tú: %1$s + Adelante + Reenviar a... + ¿Aún no cuentas con un servidor?\nHaz click aquí para obtener uno de un proveedor + Obtener el código fuente + Grupo + Invitado + Acceso de invitado + No se puede habilitar/deshabilitar el acceso de invitado. + Permite a los invitados compartir un enlace público para unirse a esta conversación. + Permitir invitados + Ingresa una contraseña + Contraseña de acceso de invitado + Error durante la configuración/desactivación de la contraseña. + Establece una contraseña para restringir quién puede usar el enlace público. + Protección con contraseña + Volver a enviar invitaciones + No se pudieron enviar las invitaciones debido a un error. + Las invitaciones se enviaron nuevamente. + Compartir enlace de la conversación + Escribe un mensaje... + Conversación importante + Mantén + Necesitas promover un nuevo moderador antes de poder abandonar la conversación + %1$s | Última modificación: %2$s + Dejar la conversación + Saliendo de la llamada... + Licencia Pública General GNU, Versión 3 + Licencia + Se ha alcanzado el límite de %s caracteres + Vestíbulo + Esta reunión está programada para %1$s + La reunión comenzará pronto + Actualmente estás esperando en el vestíbulo. + Tu ubicación actual + se requiere permiso de ubicación + Ubicación desconocida + Bloqueado + Toca para desbloquear + No establecido + Marcar como leído + Marcar como no leído + Error + Error al enviar el mensaje: + Cancelar respuesta + Mensaje leído + Mensaje enviado + Perdiste una llamada de %s + Moderador + Nueva conversación + Visibilidad + Menciones no leídas + Mensajes no leídos + %1$s no disponible (no instalado o restringido por el administrador) + Invitado + No + No hay conversaciones abiertas + No hay conversaciones abiertas a las que puedas unirte.\nO no hay conversaciones abiertas o ya te has unido a todas. + Sin proxy + No se te permite activar el audio. + No se te permite activar el video. + No ahora + %1$s en el canal de notifcación %2$s + Llamadas + Notificar sobre llamadas entrantes + Mensajes + Notificar sobre mensajes entrantes + Cargas + Notificar sobre el progreso de carga + Configuración de notificaciones + Siempre notificar + Notificar cuando se mencione + Nunca notificar + Actualmente sin conexión, por favor verifica tu conectividad + OK + También abierta a usuarios de aplicaciones invitadas + Dueño + Participantes + Agregar participantes + Abrir configuraciones + Cuenta no encontrada + Chat a través de %s + Silenciar micrófono + Habilitar micrófono + Mensajes + Privacidad + Información personal + Promover a moderador + Notificaciones push deshabilitadas + Presiona-para-hablar + Con el microfono deshabilitado, haz click & presiona para usar Presiona-para-hablar + Recuérdame más tarde + Eliminar de favoritos + Eliminar grupo y miembros + Eliminar participante + Renombrar conversación + Renombrar + Responder + Responder de forma privada + Guardar + 30 segundos + 5 minutos + 1 minuto + 10 minutos + 600 + 60 + 30 + 300 + Buscar + Seleccionar una cuenta + %1$s ha enviado un GIF. + Has enviado un GIF. + %1$s ha enviado un video. + Has enviado un video. + %1$s ha enviado un audio. + Has enviado un audio. + %1$s ha enviado una imagen. + Has enviado una imagen. + Probar la conexión del servidor + Por favor actualiza tu base de datos %1$s + Se presentó una falla al importar la cuenta seleccionada + El enlace a tu interfaz web %1$s cuando lo abras en el navegador. + Importar cuenta de la aplicación %1$s + Importar cuenta + Importar cuentas de la aplicación %1$s + Importar cuentas + Por favor saca a tu %1$s de mantenimiento + Por favor termina tu instalación de %1$s + Probando conexión + El servidor no tiene instalada la aplicación Talk compatible + Dirección del servidor https://... + %1$s solo funciona con %2$s 13 y posteriores + Configuraciones + Tu cuenta existente ha sido actualizada, en lugar de agregar una nueva + Avanzado + Apariencia + Llamadas + Instruye al teclado a desactivar el aprendizaje personalizado (sin garantías) + Teclado en modo incógnito + Sin sonido + La aplicación Talk no está instalada en el servidor al que intentaste autenticarte + Notificaciones + Mensajes + Compara los contactos según el número de teléfono para integrar el acceso directo de Talk en la aplicación de contactos del sistema + Puedes establecer tu número de teléfono para que otros usuarios puedan encontrarte + Ingresar número de teléfono + Número de teléfono no válido + El número de teléfono se estableció correctamente + Número telefónico + Integración de número de teléfono + Privacidad + servidor de proxy + Contraseña del proxy + Puerto del proxy + Tipo de proxy + Nombre de usuario del proxy + Compartir mi estado de lectura y mostrar el estado de lectura de los demás + Estado de lectura + Volver a autorizar cuenta + Eliminar + Eliminar cuenta + Confirma tu intención de eliminar la cuenta actual. + Bloquear %1$s con el bloqueo de pantalla de Android o un método biométrico compatible + Tiempo de espera de inactividad del bloqueo de pantalla + Bloqueo de pantalla + Evita las capturas de pantalla en la lista reciente y dentro de la aplicación + Seguridad de pantalla + ¡La versión del servidor es muy antigua y no será compatible en la próxima versión! + ¡La versión del servidor es demasiado antigua y no es compatible con esta versión de la aplicación de Android! + Servidor no compatible + Oscuro + Usar configuración predeterminada del sistema + tema + Claro + Tema + Compartir mi estado de escritura y mostrar el estado de escritura de los demás + El estado de escritura solo está disponible cuando se utiliza un backend de alto rendimiento (HPB) + Estado de escritura + El proxy requiere credenciales + Advertencia + Solo la cuenta actual se puede volver a autorizar + Compartir contacto + Se requiere permiso para leer contactos + Compartir ubicación actual + Compartir liga + Compartir ubicación + Compartir esta ubicación + Elige la cuenta + Elementos compartidos + Tarjeta de Deck + Imágenes, archivos, mensajes de voz... + No hay elementos compartidos + Ubicación + Ubicación compartida + Ordenar por + Hora de inicio + Cambiar cuenta + Elegir archivos + ¿Enviar estos archivos a %1$s? + ¿Enviar este archivo a %1$s? + Lo siento, error al subir + Error al subir %1$s + Error + Compartir desde %1$s + Subir desde el dispositivo + Cargando + %1$s a %2$s - %3$s\%% + Tomar foto + Tomar video + Usuario + Grabación de video desde %1$s + Grabación de Talk desde %1$s (%2$s) + Mantén presionado para grabar, suelta para enviar. + Se requiere permiso para grabar audio + « Deslizar para cancelar + Seminario web + + Semana siguiente + No hay integración de número de teléfono debido a permisos faltantes + Todos los mensajes + Solo @-menciones + Apagado + 1 hora + En línea + Estado en línea + Conversaciones abiertas + Abrir en la aplicación de archivos + Reproducir/pausar mensaje de voz + Agregar opción + Editar votación + Finalizar encuesta + ¿Realmente deseas finalizar esta encuesta? Esto no se puede deshacer. + No puedes votar con más opciones para esta encuesta. + Respuestas múltiples + Opciones + Encuesta privada + Pregunta + Tu pregunta + Resultados + Ajustes + Votar + Voto enviado + Previamente establecido + Levantar la mano + Todos + No es posible compartir archivos desde el almacenamiento sin permisos + La llamada se está grabando + Cancelar inicio de grabación + La grabación falló. Por favor, contacta a tu administrador. + Iniciar grabación + ¿Realmente deseas detener la grabación? + Detener grabación de llamada + Detener grabación + Deteniendo la grabación... + Grabación + Restablecer estado + No es posible unirse a otras salas mientras se está en una llamada + Guardar + Scan QR Code + Solo sincronizar con servidores de confianza + Federado + Solo visible para las personas en esta instancia y los invitados + Local + Solo visible para las personas que coinciden a través de la integración del número de teléfono mediante Talk en dispositivos móviles + Privado + Sincronizar con servidores de confianza y la libreta de direcciones global y pública + Publicado + Alternar ámbito + Cambiar nivel de privacidad de %1$s + Desplazarse hasta abajo + hace segundos + Seleccionado + Enviar correo electrónico + Enviar a + No tienes permiso para compartir contenido en este chat + Enviar a... + Enviar sin notificación + Establecer + Establecer avatar desde la cámara + Establecer estado + Establecer mensaje de estado + Compartir + Audio + Archivo + Multimedia + Otro + Encuesta + Grabación de llamada + Voz + Hacer favorito + No tienes permiso para iniciar una llamada + Mensaje de estado + Cambiar a sala secundaria + Cambiar a sala principal + Tomar una foto + Error al tomar la foto + No es posible tomar una foto sin permisos + Volver a tomar foto + Enviar + Cambiar de cámara + Recortar foto + Reducir tamaño de la imagen + Alternar linterna + 30 minutos + Esta semana + Este es un mensaje de prueba + Hoy + Mañana + Traducir + Traducción + Copiar texto traducido + Detectar idioma + Configuración del dispositivo + No se pudo detectar el idioma + Traducción fallida + De + A + y 1 persona más está escribiendo... + están escribiendo... + está escribiendo... + y %1$s personas más están escribiendo... + No leído + Subir nuevo avatar desde el dispositivo + Avatar de usuario + Dirección + Nombre completo + Correo electrónico + Número telefónico + Twitter + Sitio web + Estatus + Error al recuperar información personal 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. + ¿Cuál es tu estado? + + %d voto + %d votos + %d votos + + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..927505c --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,732 @@ + + + Editar + Añadir + Añadir a Notas + Se añadió la conversación %1$s a los favoritos + Buscar en %s + Aparecer como desconectado + Archivar conversación + Una vez que una conversación es archivada, estará escondida por defecto. Seleccione el filtro \"Archivadas\" para ver las conversaciones archivadas. Cualquier mención directa seguirá siendo recibida. + Archivada + %1$s Archivada + Llamada de audio + Bluetooth + Salida de audio + Teléfono + Altavoz + Auricular con cable + Su estado fue establecido automáticamente + Avatar + Ausente + Botón atras + Bloquear + Bloquear participante + Lista de bloqueados + Ocupado + Calendario + Opciones avanzadas de llamada + La llamada ha estado activa por una hora. + Llamada sin notificación + Los permisos para la cámara fueron otorgados. Por favor seleccione la cámara nuevamente. + Cancelar inicio de sesión + Elegir avatar desde la nube + Borrar el mensaje de estado + Borrar el mensaje de estado después de + Cerrar + Ícono de cerrar + Conexión establecida + Sin conexión al servidor + Se perdió la conexión - Los mensajes a enviar se encuentran encolados + Bloquear grabación para grabación continua del mensaje de voz + La conversación está archivada + La conversación es de sólo lectura + Fallo al establecer la conversación como de sólo lectura. + Conversaciones + Crear conversación + Crear informe de fallo + Personalizado + Zona de peligro + %1$s en %2$s + Borrar avatar + Eliminar grabación de voz + Se eliminó la conversación %1$s + No molestar + No borrar + Editar + Los mensajes con una antigüedad mayor a 24 horas no pueden ser editados + Editar mensaje + Reciente + Cifrado + Finalizar llamada + Finalizar llamada para todos + Hubo un problema cargando sus chats + Ocurrió un error al desbloquear al participante + Fallo al guardar %1$s + 15 minutos + carpeta + Cargando … + %1$s (%2$d) + Hilos suscritos + 4 horas + Fallo al obtener invitaciones pendientes + (editado) + Nota interna + Invisible + Los lenguajes no pudieron ser recuperados + Recuperación fallida + Más tarde hoy + Abandonar llamada + Ud. Abandonó la conversación %1$s + Cargar más resultados + Hora local: %1$s + Se denegó el permiso de ubicación + Por favor, habilítelo en los ajustes de aplicación + Servicios de ubicación desactivados + Por favor, habilite los servicios de ubicación (GPS) para utilizar esta característica + Bloquear conversación + Símbolo de bloqueo + Bajar la mano + Se marcó la conversación %1$s como leída + Se marcó la conversación %1$s como no leída + Mencionado + Nuevas primero + Antiguas primero + A - Z + Z - A + Más grandes primero + Más pequeñas primero + Mensaje copiado + ¿Está seguro que desea eliminar este mensaje? + Has eliminado este mensaje + Editado por %1$s + Pulse para abrir la encuesta + No hay resultados de búsqueda + Empiece a escribir para buscar… + Buscar … + Mensajes + Silenciar todas las notificaciones + La cuenta seleccionada ha sido importada y está disponible + Acerca de + Activar usuario + Añadir cuenta + La cuenta está programada para borrado y no se puede operar con ella + Abrir menú principal + Añadir adjunto + Añadir emojis + Añadir a la conversación + Añadir participantes + Añadir a favoritos + OK. Todo hecho. + Pin: %1$s + Desbloquear %1$s + Para habilitar los parlantes bluetooth, por favor, otorgue el permiso \"Dispositivos cercanos\". + Contestar como videollamada + Contestar como llamada de audio solamente + Cambiar salida de audio + Activar/Desactivar cámara + Colgar + Activar/Desactivar el micrófono + Abrir el modo de imagen sobre imagen, PiP + Cambiar a video propio + ENTRANTE + Nombre de la conversación + Notificaciones de llamadas + %1$s levantó la mano + Reconectándose … + LLAMANDO + %1$s en una llamada + %1$s con teléfono + %1$s con video + No ha habido respuesta en 45 segundos, pulse para intentarlo de nuevo + %s llamada + %s videollamada + %s llamada de voz + Para habilitar la comunicación por vídeo, por favor, otorgue el permiso \"Cámara\". + Cancelar + Fallo al recuperar capacidades. Abortando. + Leyenda + ¿Confías en el certificado SSL antes desconocido emitido por %1$s para %2$s, válido de %3$s a %4$s? + Comprobar el certificado + Tu configuración SSL impidió la conexión + Cambiar certificado de autenticación + Cambiar contraseña + Cancelar edición + Cancelar edición + Borrar todos los mensajes + Se han eliminado todos los mensajes + ¿Seguro que quieres borrar todos los mensajes en esta conversación? + Cambiar certificado de cliente + Configurar certificado de cliente + y + Copiar + Copiado al portapapeles + Crear + Deshabilitado + Descartar + Lo sentimos, algo ha ido mal. + Más opciones + Guardar + Saltar + Desconocido + Seleccionar certificado de autenticación + Conectando … + Hecho + Descripción de la conversación + Información de la conversación + Llamada de vídeo + Llamada de voz + Conversación no encontrada + Ajustes de la conversación + Únete a una conversación o empieza una nueva + ¡Saluda a tus amigos y colegas! + Copiar + Crear una conversación nueva + Crear votación + Usted: + Hoy + Ayer + Borrar + Eliminar todos + Eliminar conversación + Si borras la conversación, también se borrará para los demás participantes. + Eliminar mensaje + Mensaje borrado con éxito, pero puede haber sido filtrado a otros servicios + Eliminar ahora + El Usuario %1$s fue eliminado + Degradar de moderador + Grabar mensaje de voz + Enviar mensaje + Cuenta actual + Servidor + ¿La app de notificaciones de servidor está instalada? + Usuario + ¿Los estados de usuario están habilitados? + Versión de Android + Aplicación + Nombre de la app + Usuarios registrados + Versión de la App + La optimización de batería se está ignorando, todo bien + La optimización de batería está activada, lo que podría causar problemas. ¡Debería deshabilitar la optimización de batería! + Ajustes de la batería + Dispositivo + Abrir la lista de verificación de solución de problemas + Abrir pantalla de diagnóstico + Abrir dontkillmyapp.com + Última obtención de token de envío de Firebase + Última generación de token de envío de Firebase + No se ha establecido ningún token de envío de Firebase. Por favor, crea un informe de error. + Token de envío de Firebase + Los servicios de Google Play no están disponibles. Las notificaciones no están soportadas + Servicios de Google Play + Los servicios de Google Play están disponibles + Último registro de envío en el proxy de envíos + Aún no está registrado en el proxy de envío + Último registro de envío en el servidor + Aún no está registrado en el servidor + Meta-información + Generación del reporte del sistema + ¿Habilitar el canal de notificación de llamadas? + ¿Habilitar el canal de notificación de mensajes? + Permisos de notificaciones + Teléfono + Versión de Talk del servidor + Versión del servidor + Externo + Interno + Modo de señalización + Contraseña no válida + El servidor se encuentra actualmente en modo de mantenimiento. + La app está desactualizada + La app es muy antigua y ya no está soportada por este servidor. Por favor, actualice. + Actualizar + ¿Quieres volver a autorizar o eliminar esta cuenta? + Guardar este medio al almacenamiento permitirá a otras apps cualquiera acceder a él. + ¿Continuar? + No + ¿Guardar al almacenamiento? + + Nombre para mostrar no pudo ser obtenido, abortando + No se ha podido almacenar el nombre para mostrar. Abortando + Editar + Editar + Editar mensaje + Editado por admin + Menú de eventos de conversación + Programar + 8 horas + 4 semanas + Apagado + 1 día + 1 hora + 1 semana + Caducar mensajes de chat + Los mensajes de chat pueden caducarse luego de cierto tiempo. Nota: Los archivos compartidos mediante chat no serán borrados para el propietario, pero no estarán compartidos en la conversación. + Fallo al recuperar la configuración de señalización + Aceptar + Rechazar + de %1$s a las %2$s + Sin invitaciones pendientes + Tiene invitaciones pendientes + Volver + Se requieren permisos para acceso a archivos + Filtrar conversaciones + Usuario que sigue un enlace público + Tu: %1$s + Reenviar + Reenviar a … + Galería + ¿No tienes todavía un servidor?\nClic aquí para conseguir uno de un proveedor + Obtener el código fuente + Grupo + Invitado + Acceso de invitado + Imposible habilitar/deshabilitar el acceso de invitado. + Permitir a los invitados compartir un enlace público para unirse a esta conversación. + Permitir invitados + Introduzca una contraseña + Contraseña de acceso para invitados + Se encontró un error al configurar/deshabilitar la contraseña. + Establezca una contraseña para controlar quién puede usar el enlace público. + Protección con contraseña + Reenviar invitaciones + Las invitaciones no fueron enviadas debido a un error. + Las invitaciones fueron enviadas nuevamente. + Compartir enlace de la conversación + Escriba un mensaje … + La optimización de batería no se está ignorando. ¡Esto debería ser cambiado para garantizar que las notificaciones funcionen en segundo plano!. Por favor, haga clic en OK y seleccione \"Todas las apps\" -> %1$s -> No optimizar + Ignorar la optimización de batería + Conversación importante + El estado \"No molestar\" se ignorará para las conversaciones importantes + Hora inválida + Invitaciones + Unirse a conversaciones abiertas + Guardar + Debe escoger a un nuevo moderador antes de que pueda abandonar la conversación + %1$s | Modificado por última vez: %2$s + Abandonar conversación + Abandonando llamada … + Licencia General Pública de GNU (GPL), versión 3 + Licencia + Se ha alcanzado el límite de %s caracteres + Sala de espera + La reunión está programada para las %1$s + La reunión empezará en breve + Ahora estás esperando en la sala de espera. + Su ubicación actual + se requieren permisos de ubicación + Posición desconocida + Bloqueado + Toca para desbloquear + No configurado + Marcar como leído + Marcar como no leído + La conversación se ha marcado como importante + Se le ha quitado la marca de sensible a la conversación + La conversación de ha marcado como sensible + Se le ha quitado la marca de importante a la conversación + La reunión ha finalizado + El mensaje se agregó a las notas + Falló + Fallo al enviar el mensaje + Fuera de línea + Cancelar respuesta + Mensajes leídos + Enviando + Mensaje enviado + El micrófono se encuentra habilitado y se está grabando el audio + Para habilitar la comunicación por voz, por favor, otorgue el permiso \"Micrófono\". + Perdió una llamada de %s + Moderador + Nueva conversación + Visibilidad + Menciones sin leer + Mensajes no leídos + %1$s no está disponible (no se encuentra instalado o está restringido por el administrador) + Invitado + No + No hay conversaciones abiertas + No hay conversaciones abiertas a las cuales pueda unirse.\nO bien no hay conversaciones abiertas, o, ya se unió a todas ellas. + Sin proxy + ¡No tiene permiso para habilitar el audio! + ¡No tiene permiso para habilitar el video! + Ahora no + %1$s en el canal de notificación %2$s + Llamadas + Notificar llamadas entrantes + Mensajes + Notificar mensajes entrantes + Subidas + Notificar el progreso de la subida + Configuración de las notificaciones + Las notificaciones no están configuradas correctamente + Los permisos de notificaciones y las configuraciones de batería están correctamente definidas para recibir notificaciones. Si de igual manera tiene problemas para recibir notificaciones, por favor, chequee si los canales de notificación para llamadas y mensajes están habilitados. Se puede conseguir ayuda adicional en DontKillMyApp.com, o, en la lista de verificación de solución de problemas. Si esto no es de ayuda, por favor, vaya a la pantalla de diagnósticos y envíe un reporte de fallos. + Solución de problemas para las notificaciones + Notificar siempre + Notificar cuando eres mencionado + No notificar nunca + Actualmente fuera de línea, por favor, compruebe los detalles de su conexión + OK + Reunión en curso + Abrir conversación a usuarios registrados + Abrir también para usuarios invitados de la aplicación + Propietario + Participantes + Añadir participantes + Contraseña + Establecer permisos + Algunos permisos fueron denegados. + Por favor, otorgue los permisos + Abrir configuración + Por favor, otorgue los permisos en Ajustes > Permisos + Cuenta no encontrada + Chat a través de %s + Silenciar micrófono + Activar micrófono + Mensajes + Privacidad + Información Personal + Elevar a moderador + Conversación pública + Notificaciones push desactivadas + Lo sentimos, algo salió mal, el error es %1$s + Lo sentimos, algo salió mal, no se pudo obtener el mensaje push de prueba + La notificación push se envió exitosamente, Debería recibir una notificación en este dispositivo con el título \"Probando notificaciones push\" + Push-to-talk + Con el micrófono deshabilitado, mantén pulsado & para usar Push-to-talk + Recuérdamelo más tarde + Eliminar de favoritos + Eliminar grupo y miembros + Eliminar participante + Quitar Contraseña + Eliminar equipo y miembros + Renombrar conversación + Renombrar + Responder + Responder en privado + Se retuvo la sala de manera exitosa + Guardar + Guardado correctamente + 30 segundos + 5 minutos + 1 minuto + 10 minutos + Inmediato + 600 + 60 + 30 + 300 + Buscar + Limpiar búsqueda + Selecciona una cuenta + Actualizar mensaje + Enviar grabación de voz + Conversación sensible + La vista previa de los mensajes será deshabilitada en la lista de conversaciones y en las notificaciones + %1$s ha enviado un GIF. + Has enviado un GIF. + %1$s ha enviado un vídeo. + Has enviado un video. + %1$s ha enviado un audio. + Has enviado un audio. + %1$s ha enviado una imagen. + Has enviado una imagen. + %1$senvió una tarjeta de deck + Probar conexión al servidor + Por favor actualiza la base de datos de %1$s + No se ha podido importar la cuenta seleccionada + El link a tu interfaz web %1$s cuando la abras en el navegador. + Importar cuenta desde la app %1$s + Importa cuenta + Importar cuentas desde la app %1$s + Importar cuentas + Por favor pon %1$s fuera de mantenimiento + Por favor, finaliza la instalación de %1$s + Probando conexión + El servidor no tiene una app Talk soportada instalada + Dirección del servidor https://… + %1$s sólo trabaja con %2$s 13 y superior + Establecer contraseña nueva + Establecer Contraseña + Configuración + Tu cuenta ya existente ha sido actualizada en lugar de añadir una nueva + Avanzado + Apariencia + Llamadas + Por favor contacte al administrador de + Abrir la pantalla de diagnóstico para verificar los ajustes o para crear un informe de fallos + Diagnóstico + Indica que el teclado tendrá desactivado el aprendizaje personalizado (no hay garantías) + Teclado de incógnito + Sin sonido + La app Talk no está instalada en el servidor contra el que has intentado autorizarte + Notificaciones + Las notificaciones no están permitidas + Las notificaciones están permitidas + Mensajes + Comparar el número de teléfono de los contactos para integrar un acceso directo a Talk en la app de contactos del sistema + + Error 429 Demasiadas Solicitudes + Puede establecer su número de teléfono para que otros usuarios puedan encontrarlo + Introduce número de teléfono + Número de teléfono no válido + Número de teléfono guardado correctamente + Número de teléfono + Integración del número de teléfono + Privacidad + Host del proxy + Contraseña del proxy + Puerto del proxy + Tipo de proxy + Nombre de usuario para el proxy + Compartir mi estado de lectura y mostar el estado de lectura de otros + Estado de lectura + Volver a autorizar la cuenta + Eliminar + Eliminar cuenta + Por favor, confirma tu intención de eliminar la cuenta actual. + Bloquea %1$s con el bloqueo de pantalla de Android o un método biométrico soportado + Tiempo de inactividad para bloqueo de pantalla + Bloqueo de pantalla + Evita capturas de pantalla en la lista de recientes y dentro de la app + Seguridad de la pantalla + ¡La versión del servidor es muy antigua y no será compatible con la próxima versión! + La versión del servidor es demasiado antigua y no es compatible con esta versión de la app para Android + Servidor no soportado + La app de notificaciones del servidor no está instalada + Establecido por el ahorro de batería + Oscuro + Usar valores predeterminados del sistema + tema + Brillante + Tema + Compartir mi estado de escritura y mostrar el estado de escritura de otros + El estado de la escritura solo está disponible cuando se usa un backend de alto rendimiento (HPB) + Estado de la escritura + El proxy requiere credenciales + Alerta + Solo la cuenta actual puede ser reautorizada + Compartir contacto + Se requiere permiso para leer los contactos + Compartir ubicación actual + Compartir enlace + Compartir ubicación + Compartir esta ubicación + Elija una cuenta + Elementos compartidos + Tarjeta Deck + Imágenes, archivos, mensajes de voz … + No hay elementos compartidos + Ubicación + Ubicación compartida + Cuando las notificaciones no están configuradas correctamente, mostrar un aviso periódico + Mostrar aviso periódico sobre notificaciones + Ordenar por + Iniciar chat grupal + Hora de inicio + Cambiar cuenta + Equipo + Probando notificaciones push + Resultados de la prueba + Hoy a las %1$s + Mañana a las %1$s + Seleccionar archivos + ¿Enviar estos archivos a %1$s? + ¿Enviar este archivo a %1$s? + Lo siento, error en la subida + Fallo al subir %1$s + Falla + Compartido por %1$s + Subir desde dispositivo + Subiendo + %1$s a %2$s - %3$s\%% + Tomar foto + Tomar video + Usuario + Grabación de video de %1$s + Grabación Talk desde %1$s (%2$s) + Mantenga presionado para grabar, suelte para enviar. + Se requiere permiso para grabar audio + « Deslizar para cancelar + Webinar + + Semana siguiente + No hay conversaciones archivadas + No hay mensajes fuera de línea guardados + No hubo integración de la agenda telefónica debido a la falta de permisos + Todos los mensajes + Solo las menciones con @ + Apagado + Predeterminado + Seguir los ajustes de la conversación + 1 hora + En línea + Estado en línea + Conversaciones abiertas + Abrir en app Archivos + Abrir Notas + Ir al hilo + Reproducir/pausar mensaje de voz + Control de velocidad de reproducción + Añadir opción + Editar voto + Cerrar la encuesta + ¿Está seguro de que quiere cerrar esta encuesta? Esta acción no se puede deshacer. + No puede votar con más opciones para esta encuesta. + Múltiples respuestas + Borrar opción %1$d + Opción %1$d + Opciones + Encuesta privada + Pregunta + Su pregunta + Resultados + Ajustes + Votar + Voto enviado + Previamente definido + El código QR no se ha podido leer + Levantar la mano + Todo + Compartir archivos desde el almacenamiento no es posible sin permisos + Hilos recientes + La llamada está siendo grabada + Cancelar inicio de la grabación + La grabación falló. Por favor, contacte a su administrador. + Empezar la grabación + ¿Realmente desea detener la grabación? + Detener grabación de la llamada + Detener la grabación + Deteniendo grabación ... + El consentimiento para grabar es obligatorio para todas las llamadas + La grabación podría incluir su voz, vídeo de su cámara y la pantalla compartida. Se requiere su consentimiento antes de unirse a la llamada. ¿Da Ud. su consentimiento? + Obtener consentimiento para grabar antes de unirse a llamadas en esta conversación + Consentimiento de grabación + La llamada podría ser grabada. + Grabando + Se quitó la conversación %1$s de los favoritos + La conversación %1$s fue renombrada + Reenviar + Restablecer estado + No es posible unirse a otras salas mientras está en una llamada + Guardar + Escanear código QR + Sincronizar solo con servidores de confianza + Federado + Sólo visible para las personas de esta instancia e invitados + Local + Sólo visible para las personas que coincidan con la integración del número de teléfono a través de Talk en el móvil + Privado + Sincronizar con servidores de confianza y con la libreta de direcciones global y pública + Publicado + Alternar vista + Cambiar el nivel de privacidad de %1$s + Ir al final + Ícono de búsqueda + hace segundos + Seleccionado + Enviar correo electrónico + Enviar a + No tiene permiso para compartir contenido a este chat + Enviar a … + Enviar sin notificación + Establecer + Crear avatar desde la cámara + Establecer estado + Establecer mensaje de estado + Compartir + Unirse a la conversación %1$s en %2$s + Audio + Archivo + Multimedia + Otro + Encuesta + Grabación de llamada + Voz + Mostrar razón de bloqueo + Mostrar participantes bloqueados + Favorito + No está autorizado a iniciar una llamada + Crear un hilo + inició una llamada + Mensaje de estado + El estado ha sido revertido + Cambiar a sala de grupos + Cambiar a la sala principal + Tomar una foto + Error al tomar la foto + No se puede tomar una foto sin permisos + Volver a tomar foto + Enviar + Cambiar de cámara + Recortar foto + Reducir el tamaño de la imagen + Alternar linterna + 30 minutos + Esta semana + Esto es un mensaje de prueba + Este fin de semana + Cancelar la creación del hilo + Notificaciones para hilos + Responder + Título del hilo + No se encontraron hilos + Hoy + Mañana + Traducir + Traducción + Copiar texto traducido + Detectar idioma + Configuración del dispositivo + No fue posible detectar el lenguaje + La traducción falló + De + A + y 1 otro está escribiendo ... + están escribiendo ... + está escribiendo ... + y %1$s otros están escribiendo ... + Desarchivar conversación + Una vez que se desarchiva una conversación, se volverá a mostrar por defecto. + %1$s Desarchivada + Desbloquear + No leído + Subir avatar desde dispositivo + %1$s está fuera de la oficina y es posible que no responda + %1$s está fuera de la oficina hoy + Reemplazo: + Avatar del usuario + Dirección + Nombre completo + Email + Número de teléfono + Twitter + Página web + Estado + Fallo al obtener la información personal del usuario. + No se ha configurado la información personal + Añade tu nombre, imagen y detalles de contacto en tu página de perfil. + Videollamada + ¿Cuál es su estado? + + Ver %d mensaje similar + Ver %d mensajes similares + Ver %d mensajes similares + + + Esta conversación será eliminada automáticamente para todos tras %1$d día de inactividad + Esta conversación será eliminada automáticamente para todos tras %1$d días de inactividad + Esta conversación será eliminada automáticamente para todos tras %1$d días de inactividad + + + %d respuesta + %d respuestas + %d respuestas + + + %d voto + %d votos + %d votos + + diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml new file mode 100644 index 0000000..84a63ed --- /dev/null +++ b/app/src/main/res/values-et-rEE/strings.xml @@ -0,0 +1,727 @@ + + + Muuda + Lisa + Lisa Märkmetesse + „%1$s“ vestlus on märgitud lemmikuks + Otsi siin: %s + Sellega paistad olema võrgust väljas + Arhiveeri vestlus + Kui vestlus on arhiveeritud, siis on ta vaikimisi peidetud. Saad neid leida, kui kasutad filtrivalikut „Arhiveeritud“. Otsemainimiste teave liigub sellele vaatamata. + Arhiveeritud + Arhiveeritud %1$s + Häälkõne + Bluetooth + Heliväljund + Telefon + Valjuhääldi + Kaabliga kõrvaklapid + Su staatus määrati automaatselt + Tunnuspilt ehk avatar + Eemal + Tagasi-nupp + Sea suhtluskeeld + Sea osalejale suhtluskeeld + Suhtluskeelu saanute loend + Hõivatud + Kalender + Kõne lisavalikud + Kõne on kestnud üle tunni. + Kõne ilma teavituseta + Õigused kaamera kasutamiseks on nüüd olemas. Palun vali kaamera uuesti. + Katkesta sisselogimine + Vali tunnuspilt pilvest + Eemalda olekuteade + Eemalda olekuteade pärast + Sulge + Sulgemise ikoon + Saadi ühendus + Puudub ühendus serveriga + Ühendus on katkenud - saadetud sõnumid on edasisaatmise ootejärjekorras + Häälsõnumi pidevaks salvestamiseks lukusta salvestamine + Vestlus on arhiveeritud + Vestlus on vaid loetav + Ei õnnestunud muuta vestlust ainult loetavaks + Vestlused + Loo vestlus + Teata veast + Kohandatud + Ohtlik - siin ole ettevaatlik + %1$s: %2$s + Kustuta tunnuspilt + Kustuta helisalvestis + %1$s vestlus on kustutatud + Ära sega + Ära tühjenda + Muuda + Ööpäevast vanemaid sõnumeid ei saa muuta + Muuda sõnumit + Hiljutised + Krüptitud + Lõpeta kõne + Lõpeta kõne kõigi jaoks + Sinu vestluste laadimisel tekkis viga + Osaleja suhtluskeelu eemaldamisel tekkis viga + „%1$s“ salvestamine ei õnnestunud + 15 minutit + kaust + Laadimisel... + %1$s (%2$d) + Jutulõngad, mida sa jälgid + 4 tundi + Ootel kutsete laadimine ei õnnestunud + (muudetud) + Sisemine märge + Nähtamatu + Keeli ei õnnestu laadida + Laadimine ei õnnestunud + Täna hiljem + Lahku kõnest + Sa lahkusid %1$s vestlusest + Laadi veel tulemusi + Kohalik aeg: %1$s + Õigused asukoha tuvastamiseks on keelatud + Palun luba see rakenduse seadistustest + Asukohateenused pole sisse lülitatud + Selle funktsionaalsuses kasutamiseks palun luba asukohateenused (GPS) + Lukusta vestlus + Lukuikoon + Lase käsi alla + „%1$s“ vestlus on märgitud loetuks + „%1$s“ vestlus on märgitud mitteloetuks + Mainitud + Uuemad eespool + Vanemad eespool + A - Z + Z - A + Suuremad esimesena + Väiksemad esimesenaa + Sõnum on kopeeritud + Kas oled kindel, et tahad selle sõnumi kustutada? + Sina kustutasid sõnumi + %1$s muutis sõnumit + Küsitluse avamiseks klõpsi + Otsingul pole tulemusi + Otsimiseks alusta kirjutamist… + Otsi… + Sõnumid + Sellega summutad teavitused + Valitud kasutajakonto on nüüd imporditud ja saadaval + Info + Aktiivne kasutaja + Lisa konto + Kasutajakonto kustutamine on ajastatud ja seda muuta ei saa + Ava põhimenüü + Lisa manus + Lisa emojisid + Lisa vestlusele + Lisa osalejaid + Lisa lemmikutesse + Tore, kõik on tehtud! + PIN-kood: %1$s + Eemalda %1$s lukustus + Et saaksid Bluetoothi abil ühendatud kõlareid, palun luba rakendusel kasutada õigust „Seadmes lähikonnas“. + Vasta videokõnena + Vasta vaid häälkõnena + Muuda heliväljundit + Lülita kaamera sisse/välja + Lõpeta kõne + Lülita mikrofon sisse/välja + Ava pilt-pildis vaade + Näita oma videovaadet + SAABUV + Vestluse nimi + Kõne teavitus + %1$s andis käega märku + Loon uuesti ühendust… + HELISEB + %1$s vestluses + %1$s telefoniga + %1$s videoga + 45 sekundi jooksul pole vastust, uuesti proovimiseks klõpsi + „%s“ kõne + „%s“ videokõne + „%s“ häälkõne + Et saaksid suhtlemisel kasutada videoedastust, palun luba rakendusel kasutada kaamerat. + Loobu + Ei õnnestunud laadida võimekusi, katkestan + Tiiter + Kas sa usaldad seni tundmatut SSL sertifikaati, mille on %2$s jaoks andnud välja %1$s, kehtivusega %3$s kuni %4$s? + Kontrolli seda sertifikaati + Sinu SSL-i seadistused ei võimaldanud ühendust luua + Muuda autentimise sertifikaati + Muuda salasõna + Katkesta muutmine + Katkesta muutmine + Kustuta kõik sõnumid + Kõik sõnumid on kustutatud + Kas sa kindlasti soovid sellest vestlusest kõik sõnumid kustutada? + Muuda kliendi sertifikaati + Seadista kliendisertifikaat + ja + Kopeeri + Kopeeritud lõikepuhvrisse + Lisa + Keelatud + Jäta vahele + Vabandust, midagi läks valesti! + Täiendavad valikud + Lisa + Jäta vahele + Teadmata + Vali autentimise sertifikaat + Ühendan… + Valmis + Vestluse kirjeldus + Vestluse teave + Videokõne + Häälkõne + Vestlust ei leidu + Vestluse seadistused + Liitu vestlusega või alusta uut + Ütle tere oma sõpradele ja kolleegidele! + Kopeeri + Loo uus vestlus + Loo küsitlus + Sina: + Täna + Eile + Kustuta + Kustuta kõik + Kustuta vestlus + Kui kustutad selle vestluse, siis kustub see ka kõikide osalejate jaoks. + Kustuta sõnum + Sõnumi kustutamine õnnestus, kuid võis juhtuda, et ta oli juba teistesse sõnumiteenustesse edastatud + Kustuta kohe + Kasutaja „%1$s“ on eemaldatud + Võta ära moderaatori õigused + Salvesta häälsõnum + Saada sõnum + Praegune kasutajakonto + Server + Kas serveriteavituste rakendus on paigaldatud? + Kasutaja + Kas kasutaja olek on kasutusel? + Androidi versioon + Rakendus + Rakenduse nimi + Registreeritud kasutajad + Rakenduse versioon + Akukasutuse optimeerimine on eiratud, kõik on korras + Akukasutuse optimeerimine on sisse lülitatud. Võimalike probleemide vältimiseks lülita ta selle rakenduse jaoks välja! + Akuseadistused + Seade + Ava veaotsingu kontrollnimekiri + Ava diagnostikavaade + Ava dontkillmyapp.com + Firebase\'i tõuketeenuste tunnusloa viimane laadimine + Firebase\'i tõuketeenuste tunnusloa viimane loomine + Firebase\'i tõuketeenuste tunnusluba on puudu. Palun koosta asjakohane veateade. + Firebase\'i tõuketeenuste tunnusluba + Google Play teenused pole saadaval. Teavitused pole toetatud + Google Play teenused + Google Play teenused on saadaval + Viimane registreerimine tõuketeenuste vahendajas + Pole veel registreeritud tõuketeenuste vahendajas + Tõuketeenuste viimane registreerimine serveris + Pole veel serveris registreeritud + Metateave + Süsteemiaruande genereerimine + Kas kõnedest teavitamise kanal on kasutusel? + Kas sõnumitest teavitamise kanal on kasutusel? + Teavituste õigused + Telefon + Talk-rakenduse versioon serveris + Serveri versioon + Väline + Sisene + Signaliseerimisrežiim + Vale salasõna + Server on hetkel hooldusrežiimis. + Rakendus on aegunud + See rakendus on liiga vana ja pole serveri poolt enam toetatud. Palun uuenda rakendust. + Uuenda + Kas sa soovid seda kasutajakontot uuesti autoriseerida või kustutada? + Salvestades selle meediafaili andmeruumi annad võimaluse muudel selle nutiseadme rakenduste teda kasutada. + Kas jätkame? + Ei + Kas salvestame andmeruumi? + Ja + Kuvatavat nime ei õnnestunud laadida, katkestan + Kuvatavat nime ei õnnestunud salvestade, katkestan + Muuda + Redigeeri + Muuda sõnumit + Muudetud peakasutaja poolt + Sündmuse vestluse menüü + Ajakava + 8 tundi + 4 nädalat + Pole kasutusel + 1 päev + 1 tund + 1 nädal + Lase vestluse sõnumeil aeguda + Vestluse sõnumid saad määrata kustuma etteantud aja järel. Palun arvesta, et sel juhul vestluses jagatud failid jäävad omaniku jaoks nähtavaks, aga pole enam vestluse jaoks jagatud. + Kõnehõlbustuse seadistusi ei õnnestunud laadida + Nõustu + Keeldu + kasutajalt %1$s, %2$s + Ootel kutseid pole + Sul on ootel kutseid + Tagasi + Vajalikud on õigused ligipääsuks failile + Filtreeri vestlusi + Kasutaja, kes järgnes avalikule lingile + Sina: %1$s + Edasi + Edasta saajale… + Galerii + Kas sul ei ole veel oma serverit?\nVajuta siia, et tutvuda teenusepakkujatega + Vaata lähtekoodi + Grupp + Külaline + Ligipääs külalistele + Külaliste ligipääsu sisse/välja lülitamine ei õnnestu. + Luba külalistel selle vestlusega liitumiseks jagada avalikku linki. + Luba külalised + Sisesta salasõna + Salasõna külalisele + Viga salasõna salvestamisel või eemaldamisel. + Määra salasõna, millega saad piirata avaliku lingi kasutajaid. + Kaitstud salasõnaga + Saada kutsed uuesti + Kutsed jäid vea tõttu saatmata. + Kutsed on saadetud uuesti + Jaga vestluse linki + Koosta sõnum… + Akukasutuse optimeerimine pole eiratud. Et teavituste saatmine toimiks taustal, siis peaksid seda muutma. Palun klõpsi „Sobib“ ja vali „Kõik rakendused“ → „%1$s“ → „Ära optimeeri“ + Eira akukasutuse optimeerimist + Oluline vestlus + „Ära sega“ olek on oluliste vestluste puhul eiratud + Vigane aeg + Kutsed + Liitu avalike vestlustega + Hoia alles + Pead määrtama uue moderaatori enne, kui saad siis vestlusest lahkuda + %1$s | Viimati muudetud: %2$s + Lahku vestlusest + Lahkun kõnest… + GNU Üldine Avalik Litsents, versioon 3 + Litsents + Käes on %s tähemärgi ülempiir + Ooteruum + Selle kohtumise algusaeg on %1$s + Kohtumine algab varsti + Sa asud hetkel ooteruumis + Sinu praegune asukoht + Vajalikud on õigused asukoha tuvastamiseks + Asukoht pole teada + Lukus + Klõpsi lukustuse eemaldamiseks + Pole määratud + Märgi loetuks + Märgi mitteloetuks + Vestlus on märgitud oluliseks + Vestlus on märgitud mittedelikaatseks + Vestlus on märgitud delikaatseks + Vestlus on märgitud mitteoluliseks + Kohtumine on lõppenud + Teade on lisatud märkmetesse + Ebaõnnestus + Sõnumi saatmine ei õnnestunud: + Pole võrgus + Katkesta vastamine + Sõnum on loetud + Saatmisel + Sõnum on saadetud + Mikrofon on lülitatud sisse ja heli on salvestamisel + Et saaksid suhtlemisel kasutada heliedastust, palun luba rakendusel kasutada mikrofoni. + Vastamata kõne kasutajalt %s + Moderaator + Uus vestlus + Nähtavus + Lugemata mainimised + Lugemata sõnumid + „%1$s“ pole saadaval (kas pole paigaldatud või on peakasutaja piiranud nähtavust) + Külaline + Ei + Pole avalikke vestlusi + Pole avalikke vestlusi, millega saaksid liituda.\nSee siis tähendab, et kas neid pole üldse või oled kõikide olemasolevatega juba liitunud. + Proksiserver pole vajalik + Sul pole lubatud heliriba sisse lülitada! + Sul pole lubatud videot sisse lülitada! + Mitte praegu + %1$s teavituskanalis „%2$s“ + Kõned + Teavita saabuvatest kõnedest + Sõnumid + Teavita saabuvatest sõnumitest + Üleslaadimised + Teavita üleslaadimise edenemisest + Teavituse seadistused + Teavitused pole korrektselt seadistatud + Teavituste ja akukasutuse seadistused on teavituste toimimiseks korrektselt seadistatud. Kui ikkagi on nendega probleeme, siis vaata, kas kõnede ja sõnumite teavituskanalid on sisse lülitatud. Lisateavet leiad saidist DontKillMyApp.com või veaotsingu kontrollnimekirjast. Kui ka sellest pole ikkagi abi, siis ava veaotsingu vaade ja koosta veateade. + Teavituste veaotsing + Teavita alati + Teavita minu mainimisel + Ära iialgi teavita + Nutiseade pole võrgus, palun kontrolli internetiühendust + OK + Pooleliolev kohtumine + Ava vestlus registreeritud kasutajatele + Lisaks ava ka külalisrakenduse kasutajatele + Omanik + Osalejad + Lisa osalejaid + Salasõna + Lisa õigusi + Mõned õigused puuduvad. + Palun anna vajalikud õigused + Ava seadistused + Palun luba vaajalikud õigused menüüst „Seadistused“ → „Õigused“ + Kasutajakontot ei leidu + Vestle %s vahendusel + Summuta mikrofon + Lülita mikrofon sisse + Sõnumid + Privaatsus + Isiklik teave + Määra moderaatoriks + Avalik vestlus + Tõuketeavitused pole kasutusel + Vabandust, midagi läks valesti, veateade on: %1$s + Vabandust, midagi läks valesti, tõuketeavituse testsõnumit ei õnnestu laadida + Tõuketeavituse saatmine õnnestus. Peaksid selles seadmes nüüd saama tõuketeavituse pealkirjaga „Tõuketeavituse test“ + Raadiosaatja režiim + Raadiosaatja režiimi kasutamine: kui mikrofon on lülitatud välja, siis vajuta seda nuppu + Tuleta mulle hiljem meelde + Eemalda lemmikutest + Eemalda gruppe ja liikmeid + Eemalda osalejaid + Eemalda salasõna + Eemalda tiim ja liikmed + Muuda vestluse nime + Muuda nime + Vasta + Vasta privaatselt + Jututoa allesjätmine õnnestus + Salvesta + Salvestamine õnnestus + 30 sekundit + 5 minutit + 1 minut + 10 minutit + Kohene + 600 + 60 + 30 + 300 + Otsi + Tühjenda otsing + Otsi kasutajakontot + Uuenda sõnumit + Saada helisalvestis + Vestlus tundlikul teemal + Sõnumite eelvaated saava vestluste loendis ja teavitustes olema peidetud + %1$s saatis gif-faili. + Sina saatsid gif-faili. + %1$s saatis video. + Sina saatsid video. + %1$s saatis helifaili. + Sina saatsid helifaili. + %1$s saatis pildi. + Sina saatsid pildi. + %1$s saatis kanbani kaardi + Testi ühendust serveriga + Palun uuenda oma %1$si andmebaasi + Valitud kasutajakonto importimine ei õnnestunud + See on sinu %1$s kasutajaliidese veebiaadress, kui sa avad ta veebibrauseris. + Impordi kasutajakonto rakendusest %1$s + Impordi kasutajakonto + Impordi kasutajakontod rakendusest %1$s + Impordi kasutajakontod + Palun lõpeta oma %1$si hooldusrežiim + Palun lõpeta oma %1$si paigaldus + Ühenduse testimine + Serverisse pole paigaldatud toetatud kõneteenuse rakendust + Serveri aadress https://… + %1$s toimib vaid siis, kui kasutusel on %2$s 13 ja uuem + Määra uus salasõna + Määra salasõna + Seadistused + Uue kasutajakonto lisamise asemel on sinu olemasolev konto uuendatud + Täiendavad seadistused + Välimus + Kõned + Palun võta ühendust serveri administraatoriga - + Seadistuste kontrollimiseks või veateate koostamiseks ava veaotsingu vaade + Veaotsing + Sellega annad klahvistikul käsu mitte kasutada isikustatud sisestuste õpet (aga puudub garantii toimivuse osas) + Inkognito klahvistik + Heli puudub + Nextcloud Talk rakendus pole paigaldatud serverisse, mille autentimist kasutasid + Teavitused + Teavitused pole lubatud + Teavitused on lubatud + Sõnumid + Otsi telefoni kontaktide andmete ja Nextcloud Talk rakenduse andmete vasteid telefoninumbri alusel + Viga 429 - liiga palju päringuid + Võid lisada oma telefoninumbri, mille alusel teised kasutajad saavad sind leida + Sisesta telefoninumber + Vigane telefoninumber + Telefoninumbri lisamine õnnestus + Telefoninumber + Telefoninumbri lõiming + Privaatsus + Proksiserveri aadress + Proksiserveri salasõna + Proksiserveri port + Proksiserveri tüüp + Proksiserveri kasutajanimi + Jagaminu lugemise olekuid ning näita teiste kasutajate omi + Lugemise olek + Autoriseeri kasutajakonto uuesti + Eemalda + Eemalda konto + Palun kinnita oma praeguse kasutajakonto eemaldamise soovi + Kasuta %1$s rakenduse lukustamiseks Androidi ekraanilukustust või biomeetrilise tuvastuse meetodeid + Ekraani jõudeoleku kestus ekraanilukustuse käivitamiseks + Ekraanilukustus + Keela ekraanipiltide tegemine hiljutiste rakenduste loendist ja rakenduse sisevaadetest + Ekraaniturvalisus + Kasutatav serveri versioon on väga vana ning selle rakenduse järgmine versioon seda ei toeta! + Kasutatav serveri versioon on liiga vana ning selle rakenduse see versioon seda ei toeta! + Mittetoetatud server + Serveriteavituste rakendus pole paigaldatud + Määratud askukasutuse piiraja poolt + Tume kujundus + Kasuta süsteemi kujundust + kujundus + Hele kujundus + Kujundus + Jagaminu kirjutamisteatisi ning näita teiste kasutajate omi + Kirjutamisteatised on kasutatavad vaid siis, kui kasutusel on suure jõudlusega taustateenus (HPB) + Kirjutamisteatised + Proksiserveri kasutamine eeldab tuvastamist + Hoiatus + Uuesti autoriseerida saad hetkel kasutatavat kontot + Jaga kontakti + Vajalikud on õigused kontaktide lugemiseks + Jaga praegust asukohta + Jaga linki + Jaga asukohta + Jaga seda asukohta + Vali konto + Jagatud objektid + Kanbani kaart + Pildid, failid, häälsõnumid… + Jaosmeediat ei leidu + Asukoht + Jagatud asukoht + Näita tavalist hoiatust, kui teavitused pole korrektselt seadistatud + Näita tavalist teavituse hoiatust + Sorteeri + Alusta rühmavestlust + Algusaeg + Vaheta kasutajakontot + Tiim + Tõuketeavituse test + Testi tulemused + Täna kell %1$s + Homme kell %1$s + Vali failid + Kas saadame need failid kasutajale „%1$s“? + Kas saadame selle faili kasutajale „%1$s“? + Vabandust, üleslaadimine ei õnnestunud + „%1$s“ üleslaadimine ei õnnestunud + Ebaõnnestumine + Jaga teenusest %1$s + Laadi üles seadmest + Üleslaadimine + %1$s: %2$s - %3$s\%% + Pildista + Filmi + Kasutaja + Videosalvestus kasutajalt %1$s + Salvestus: %1$s (%2$s) + Salvestamiseks hoia nupp all ja saatmiseks vabasta nupp + Vajalikud on õigused heli salvestamiseks + « Katkestamiseks viipa + Veebiseminar + Jah + Järgmine nädal + Arhiveeritud vestlusi pole + Ühtegi vallasrežiimis salvestatud sõnumit ei leidu + Telefoninumbrite lõiming ei toimi puuduvate õiguste tõttu + Kõik sõnumid + Vaid @-mainimised + Pole kasutusel + Vaikimisi + Järgi vestluse seadistusi + 1 tund + Online + Võrgus staatus + Ava vestlused + Ava failirakenduses + Ava märkmik + Ava jutulõng + Esita häälsõnumit või peata esitus + Taasesituse kiiruse juhtimine + Lisa valik + Muuda oma häält + Lõpeta küsitlus + Kas oled kindel, et soovid selle küsitluse lõpetada? Seda tegevust ei saa tagasi pöörata. + Sa ei saa selle küsitluse puhul hääletada lisavalikutega. + Mitu vastust + Kustuta %1$d valik + %1$d valik + Eelistused + Privaatne küsitlus + Küsimus + Sinu küsimus + Tulemused + Seadistused + Hääleta + Hääletatud + Varasemalt seatud + QR-koodi lugemine pole võimalik + Anna käega märku + Kõik + Andmeruumist failide jagamine pole võimalik ilma vastavate õigusteta + Hiljutised jutulõngad + Kõne on salvestamisel + Katkesta salvestamise alustamine + Kõne salvestamine ei õnnestunud. Palun võta ühendust oma peakasutajaga. + Alusta salvestamist + Kas sa kindlasti soovid salvestamise lõpetada? + Lõpeta kõne salvestamine + Lõpeta salvestamine + Salvestamine on lõpetamisel… + Kõnega liitumiseks on vajalik, et nõustud salvestamisega + Salvestuses võib olla kuulda sinu häält, pilti sinu kaamerast ja ekraanivaatest. Enne kõnega liitumist on vajalik sinu nõusolek. Kas sa oled sellega nõus? + Selle vestlusega liitumisel eelda nõustumist salvestamisega + Nõusolek salvestamisega + Kõne võib olla salvestatud. + Salvestan + „%1$s“ vestlus on eemaldatud lemmikute seast + „%1$s“ vestluse nimi on muutunud + Saada uuesti + Lähesta olek + Käimasoleva kõne ajal ei saa liituda teiste jututubadega + Salvesta + Skaneeri QR-koodi + Sünkrooni vaid usaldusväärsetesse serveritesse + Federated + Nähtav vaid selle serveri kasutajatele ja külalistele + Kohalik + Nähtav vaid osalejatele, kes on läbi teinud tuvastamise telefoninumbri alusel kasutades Nextcloud Talk rakendust + Privaatne + Sünkrooni usaldusväärsetesse serveritesse ning üldisesse ja avalikku aadressiraamatusse + Avaldatud + Käsitlusala sisse/välja + Muuda „%1$s“ privaatsuse taset + Keri alla äärde + Otsinguikoon + sekundit tagasi + Valitud + Saada e-kiri + Saada... + Sul pole õigust selle vestluse sisu jagada + Saada... + Saada ilma teavituseta + Lisa + Lisa tunnuspilt kaamera abil + Määra staatus + Sea staatuse sõnum + Jaga + Liitu „%1$s“ vestkusega: %2$s + Helid + Fail + Meedia + Muu + Küsitlus + Kõnesalvestus + Hääl + Näita suhtluskeelu seadmise põhjus + Näita suhtluskeelu saanud osalejaid + Lemmik + Sul pole luba kõne algatamiseks + Alusta jutulõnga + helistas + Staatuse teade + Olek on tagasi pööratud + Mine töötuppa + Mine põhilisse jututuppa + Pildista + Viga pildistamisel + Pildistamine pole võimalik ilma vastavate õigusteta + Pildista uuesti + Saada + Vaheta kaamerat + Kadreeri fotot + Vähenda pildi suurust + Lülita taskulamp sisse/välja + 30 minutit + Käesolev nädal + See on testsõnum + See nädalavahetus + Katkesta jutulõnga koostamine + Jutulõngade teavitused + Vasta + Jutulõnga pealkiri + Ühtegi jutulõnga ei leidu + Täna + Homme + Tõlgi + Tõlge + Kopeeri tõlgitud tekst + Tuvasta keel + Seadme seadistused + Ei suutnud keelt tuvastada + Tõlkimine ebaõnnestus + Saatja + Saaja + ja veel 1 osaleja kirjutab… + kirjutavad… + kirjutab… + ja veel %1$s osalejat kirjutavad… + Võta vestlus arhiivist välja + Kui vestlus on arhiivist välja võetud, siis on ta jälle vaikimisi näha. + %1$s on arhiivist välja võetud + Eemalda suhtluskeeld + Lugemata + Lisa uus tunnuspilt arvutist või nutiseadmest + %1$s pole kohal ja ei pruugi vastata + %1$s pole täna kohal + Asendus: + Kasutaja tunnuspilt + Aadress + Täisnimi + Epost + Telefoninumber + Twitter + Veebileht + Staatus + Kasutaja isiklike andmete laadimine ei õnnestunud. + Isiklikud andmed on kirjeldamata + Lisa oma profiili lehele nimi, pilt ja kontaktteave. + Videokõne + Mis on su staatus? + + Vaata %d sarnast sõnumit + Vaata %d sarnast sõnumit + + + See vestlus kustub automaatselt kõigi osalejate jaoks, kui siin pole olnud tegevust %1$d päeva jooksul. + See vestlus kustub automaatselt kõigi osalejate jaoks, kui siin pole olnud tegevust %1$d päeva jooksul. + + + %d vastus + %d vastust + + + %d hääl + %d häält + + diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000..f8aa767 --- /dev/null +++ b/app/src/main/res/values-eu/strings.xml @@ -0,0 +1,590 @@ + + + Aldatu + Gehitu + Gehitu oharretara + %1$s elkarrizketa gogokoetara gehitu da + Bilatu %s(e)n + Artxibatu elkarrizketa + Artxibatuta + Bluetooth + Audio irteera + Telefonoa + Bozgorailua + Kabledun entzungailua + Zure egoera automatikoki ezarriko dira + Avatarra + Kanpoan + Debekatu + Debekatu parte-hartzailea + Lanpetua + Egutegia + Dei-ezarpen aurreratuak + Deia ordubetez egon da martxan. + Deitu jakinarazpenik gabe + Kameraren baimena eman da. Aukeratu kamera berriro. + Utzi saioa hasiera + Aukeratu avatarra cloudetik + Garbitu egoera-mezua + Garbitu egoera mezua ondoren + Itxi + Konexioa ezarri da + Blokeatu grabazioa ahots-mezua etengabe grabatzeko + Elkarrizketak + Sortu elkarrizketa + Sortu arazoa + Pertsonalizatua + Arrisku eremua + Ezabatu avatarra + %1$s elkarrizketa ezabatu da + Ez molestatu + Ez garbitu + Editatu + Editatu mezua + Azkenak + Zifratua + Amaitu deia + Amaitu deia denontzat + Arazo bat gertatu da zure txatak kargatzean + %1$s gordetzeak huts egin du + 15 minutu + karpeta + Kargatzen … + %1$s(%2$d) + 4 ordu + (editatuta) + Barneko oharra + Ikusezina + Ezin izan dira hizkuntzak berreskuratu + Ezin izan da berreskuratu + Beranduago gaur + Utzi deia + %1$s elkarrizketa utzi duzu + Kargatu emaitza gehiago + Blokeatu elkarrizketa + Blokeo sinboloa + Jaitsi eskua + Markatu %1$s elkarrizketa irakurrita gisa + Markatu %1$s elkarrizketa ez irakurrita gisa + Aipatuta + Berrienak lehenengo + Zaharrenak lehenengo + A - Z + Z - A + Handienak lehenengo + Txikienak lehenengo + Mezua ezabatu duzu + Sakatu galdeketa irekitzeko + Ez dago bilaketaren emaitzarik + Hasi idazten bilatzeko … + Bilatu … + Mezuak + Aukeratutako kontua inportatu da eta eskuragarri dago + Honi buruz + Erabiltzaile aktiboa + Gehitu kontua + Kontua ezabatzeko programatuta dago eta ezin da harekin operatu + Ireki menu nagusia + Gehitu eranskina + Gehitu emojiak + Gehitu elkarrizketara + Gehitu parte-hartzaileak + Gehitu gogokoetara + Ados, dena eginda! + Pin: %1$s + Desblokeatu %1$s + Bluetooth bozgorailuak gaitzeko, eman \"Inguruko gailuak\" baimena. + Erantzun bideo-dei gisa + Erantzun ahots-dei gisa soilik + Aldatu audio irteera + Aldatu kamera + Eseki + Aldatu mikrofonoa + Ireki irudiz irudi modua + Aldatu norberaren bideora + SARRERAKOA + Elkarrizketaren izena + Dei jakinarazpenak + %1$s-k eskua altxatu du + Berriro konektatzen … + DEITZEN + %1$s deian + %1$s telefonoarekin + %1$s bideoarekin + Ez da erantzunik egon 45 segundutan, sakatu berriro saiatzeko + %s deia + %s bideo-deia + %s ahots-deia + Bideo komunikazioa gaitzeko, eman \"Kamera\" baimena. + Utzi + Akatsak gaitasunak berreskuratzean. Bertan behera uzten. + Epigrafea + Fio zara %1$s (e)k %2$s(e)ntzat emandako SSL ziurtagiri ezezagunaz?, %3$s tik %4$sra baliagarria? + Egiaztatu ziurtagiria + Zure SSL konfigurazioak konexioa eragotzi du + Aldatu autentifikazio ziurtagiria + Aldatu pasahitza + Utzi editatzeari + Utzi editatzeari + Ezabatu mezu guztiak + Mezu guztiak ezabatu dira + Ziur zaude elkarrizketa honen mezu guztiak ezabatu nahi dituzula? + Aldatu bezeroaren ziurtagiria + Konfiguratu bezeroaren ziurtagiria + eta + Kopiatu + Kopiatu arbelera + Sortu + Desgaituta + Baztertu + Sentitzen dugu, zerbait gaizki joan da! + Aukera gehiago + Ezarri + Jauzi + Ezezaguna + Aukeratu autentifikazio ziurtagiria + Konektatzen … + Egina + Elkarrizketaren deskribapena + Elkarrizketen informazioa + Bideo-deia + Ahots-deia + Ez da elkarrizketa aurkitu + Elkarrizketaren ezarpenak + Batu elkarrizketa batera edo hasi berri bat + Esan kaixo zure lagun eta lankideei! + Kopiatu + Sortu elkarrizketa berri bat + Sortu galdeketa + Gaur + Atzo + Ezabatu + Ezabatu denak + Ezabatu elkarrizketa + Elkarrizketa hau ezabatzen baduzu beste kideentzat ere ezabatuko da. + Mezua ezabatzen + Mezua ondo ezabatu da, baina baliteke beste zerbitzu batzuetara filtratu izana + %1$s erabiltzailea kendu da + Moderatzailetik degradatua + Grabatu ahots mezua + Bidali mezua + Uneko kontua + Zerbitzaria + Erabiltzailea + Android bertsioa + Aplikazioa + Aplikazioaren izena + Erregistratutako erabiltzaileak + Aplikazioaren bertsioa + Bateria ezarpenak + Gailua + Ireki dontkillmyapp.com + Meta informazioa + Jakinarazpen baimenak + Telefonoa + Zerbitzariaren bertsioa + Kanpokoa + Barnekoa + Seinalizazio modua + Pasahitz baliogabea + Zerbitzaria mantentze moduan dago. + Aplikazioa zaharkituta dago + Aplikazio hau zaharregia da eta ez dago zerbitzari honengatik onartuta. Mesedez eguneratu. + Eguneratu + Kontu hau birbaimendu edo ezabatu nahi duzu? + Multimedia hau biltegian gordetzen baduzu, zure gailuko beste edozein aplikaziok atzitu ahal izango du. + Jarraitu? + Ez + Biltegian gorde nahi duzu? + Bai + Ezin izan da erakusteko izena lortu, bertan behera uzten + Ezin izan da erakusteko izena biltegiratu, bertan behera uzten + Editatu + Aldatu + Editatu mezua + 8 ordu + 4 aste + Desaktibatu + Egun 1 + Ordu 1 + Aste 1 + Iraungi txat-mezuak + Txat-mezuak denbora jakin baten ondoren iraungi daitezke. Oharra: txatean partekatutako fitxategiak ez dira jabearentzat ezabatuko, baina ez dira gehiago elkarrizketan partekatuko. + Akatsa seinaleen konfigurazioa berreskuratzean + Onartu + Ukatu + Ez duzu gonbidapenik zain + Itzuli + Fitxategietan sartzeko baimena behar da + Erabiltzailea esteka publiko bat jarraitzen ari da + Zu: %1$s + Birbidali + Birbidali … + Galeria + Oraindik zerbitzaririk gabe?\n Egin klik hemen hornitzailetik bat lortzeko + Iturburu kodea eskuratu + Taldea + Gonbidatua + Gonbidatuen sarbidea + Ezin da gaitu/desgaitu gonbidatuen sarbidea. + Baimendu gonbidatuei esteka publiko partekatu baten elkarrizketa honetan sartzea. + Baimendu gonbidatuak + Sartu pasahitza + Gonbidatua sartzeko pasahitza + Errore bat gertatu da pasahitza ezartzean/desgaitzean. + Ezarri pasahitz bat, esteka publikoa nork erabili mugatzeko. + Pasahitz bidezko babesa + Birbidali gonbidapenak + Gonbidapenak ez dira bidali errore baten ondorioz. + Gonbidapenak berriro bidali dira. + Partekatu elkarrizketa esteka + Idatzi mezu bat … + Elkarrizketa garrantzitsua + Gonbidapenak + Sartu elkarrizketa irekietara + Moderatzaile berri bat sustatu behar duzu elkarrizketatik irten aurretik + %1$s | Azkenengoz aldatuta: %2$s + Atera elkarrizketatik + Deia uzten … + GNU General Public Lizentzia, 3. bertsioa + Lizentzia + %s karaktere limitera iritsi da + Sarrera + Bilera hau %1$s(r)etarako programatuta dago + Bilera laster hasiko da + Sarreran itxaroten zabiltza momentu honetan. + Zure uneko kokalekua + Kokalekua atzitzeko baimena beharrezkoa da + Kokapen ezezaguna + Blokeatuta + Sakatu desblokeatzeko + Ezarri gabe + Markatu irakurritako gisa + Markatu ez irakurritako gisa + Huts egin du + Mezua bidaltzeak huts egin du: + Lineaz kanpo + Utzi erantzuna bertan behera + Mezua irakurrita + Mezua bidalita + Ahots bidezko komunikazioa gaitzeko, eman \"Mikrofonoa\" baimena. + %s-ren dei bat galdu duzu + Moderatzailea + Elkarrizketa berria + Ikusgarritasuna + Irakurri gabeko aipamenak + Irakurri gabeko mezuak + %1$s ez dago erabilgarri (administratzaileak instalatu edo mugatu gabe) + Gonbidatua + Ez + Ez ireki elkarrizketak + Ez dago sartu ahal zaren elkarrizketa irekirik.\nEz dago elkarrizketa irekirik edo dagoeneko denetara sartu zara. + Proxyrik ez + Ez duzu baimenik audioa gaitzeko! + Ez duzu baimenik bideoa gaitzeko! + Orain ez + %1$s %2$s jakinarazpen kanalean + Deiak + Jakinarazi sarrera-deiei buruz + Mezuak + Jakinarazi sarrerako mezuei buruz + Igoerak + Jakinarazi igoeraren egoeraren berri + Jakinarazpen-ezarpenak + Jakinarazi beti + Jakinarazi aipatzen nautenean + Ez jakinarazi inoiz + Lineaz kanpo zaude momentu honetan, mesedez egiaztatu zure konexioa + Ados + Ireki elkarrizketa erregistratutako erabiltzaileei + Ireki baita ere aplikazio gonbidatuentzat + Jabea + Parte-hartzaileak + Gehitu parte-hartzaileak + Pasahitza + Ezarri baimenak + Baimen batzuk ukatu egin dira. + Eman baimenak, mesedez + Ireki ezarpenak + Mesedez, eman baimenak Ezarpenak > Baimenak atalean + Ez da kontua aurkitu + Txateatu %s bidez + Mututu mikrofonoa + Gaitu mikrofonoa + Mezuak + Pribatutasuna + Informazio pertsonala + Moderatzaile bihurtu zara + Elkarrizketa publikoa + Push jakinarazpenak desgaiturik + Sakatu-hitz-egiteko + Mikrofonoa desgaituta daukazunean, egin klik &mantendu sakatzerakoan hitz egiteko + Gogoratu geroago + Kendu gogokoetatik + Kendu taldea eta kideak + Kendu parte-hartzailea + Kendu lantaldea eta kideak + Aldatu izena elkarrizktari + Berrizendatu + Erantzun + Erantzun pribatuki + Gorde + Ondo gorde da + 30 segundo + 5 minutu + Minutu 1 + 10 minutu + 600 + 60 + 30 + 300 + Bilatu + Garbitu bilaketa + Hautatu kontua bat + %1$s-k GIF bat bidali du. + GIF bat bidali duzu. + %1$s-k bideoa bidali du. + Bideo bat bidali duzu. + %1$s-k audio bat bidali du. + Audio bat bidali duzu. + %1$s-k irudi bat bidali du. + Irudi bat bidali duzu. + Probatu zerbitzariaren konexioa + Mesedez eguneratu zure %1$s datu-basea + Ezin izan da aukeratutako kontua inportatu + Zure %1$s web interfazerako esteka arakatzailean irekitzen duzunean. + Inportatu kontua %1$s apptik + Inportatu kontua + Inportatu kontuak %1$s apptik + Inportatu kontuak + Jarri mesedez %1$s mantenutik kanpo + Mesedez bukatu zure %1$s instalazioa + Konexioa probatzen + Zerbitzariak ez du Talk aplikazio bateragarririk instalatuta + Zerbitzariaren helbidea https://… + %1$s (e)k %2$s 13rekin eta goragokoarekin bakarrik egiten du lan + Ezarri pasahitz berria + Ezarpenak + Zure kontua eguneratu egin da, beste kontu bat gehitu beharrean + Aurreratua + Itxura + Deiak + Diagnostikoa + Teklatuaren ikasketa pertsonalizatua desgaitzen du (bermerik gabe) + Teklatu ezkutua + Soinurik ez + Talk aplikazioa ez dago instalatuta baimendu nahi duzun zerbitzarian + Jakinarazpenak + Mezuak + Lotu telefono zenbakiaren araberako kontaktuak Talk lasterbidea sistemako kontaktuen aplikazioan integratzeko + 429 errorea. Eskaera gehiegi + Zure telefono zenbakia ezarri dezakezu beste erabiltzaile batzuek aurkitu ahal izateko + Sartu telefono zenbakia + Telefono-zenbaki baliogabea + Telefono zenbakia ondo ezarri da + Telefono zenbakia + Telefono zenbaki integrazioa + Pribatutasuna + Proxy ostalaria + Proxy-pasahitza + Proxy portua + Proxy mota + Proxy erabiltzaile-izena + Partekatu nire irakurtze-egoera eta erakutsi besteen irakurtze-egoera + Irakurtze egoera + Berriro baimendu kontua + Ezabatu + Ezabatu kontua + Mesedez, berretsi oraingo kontua ezabatzeko asmoa duzula. + Giltzatu %1$s Androiden pantaila giltzapen edo onartutako metodo biometrikoarekin + Jardun gabeko denbora-muga pantaila blokeatzeko + Pantailaren blokeoa + Azken elementuen zerrenda eta app barnean pantaila-argazkiak ateratzea galarazten du + Pantailaren segurtasuna + Zerbitzariaren bertsioa zaharregia da eta ez da hurrengo argitalpenean onartuko! + Zerbitzariaren bertsioa zaharregia da eta Android aplikazio honek ez du onartzen + Ez da zerbitzaria onartzen + Iluna + Erabili sistemaren lehenetsia + Gaia + Argia + Gaia + Partekatu nire idazte-egoera eta erakutsi besteen idazte-egoera + Idazte egoera errendimendu handiko azpiegituretan (HPB) soilik dago eskuragarri + Idazte egoera + Proxyak egiaztagiriak behar ditu + Oharra + Oraingo kontua baino ezin da berrbaimendu + Partekatu kontaktua + Kontaktuak irakurtzeko baimena behar da + Partekatu uneko kokapena + Partekatu esteka + Partekatu kokapena + Partekatu kokapen hau + Aukeratu kontua + Partekatutako elementuak + Deck txartela + Irudiak, fitxategiak, ahots-mezuak … + Ez dago partekatutako elementurik + Kokapena + Partekatutako kokalekua + Ordenatu honen arabera + Hasiera-ordua + Aldatu kontua + Lantaldea + Hautatu fitxategiak + Fitxategi hauek bidali %1$s(r)i? + Fitxategi hau bidali %1$s(r)i? + Sentitzen dugu, igoerak huts egin du. + %1$s-ren igoerak huts egin du + Hutsegitea + Partekatu %1$s(e)tik + Igo gailutik + Igotzen + %1$s %2$s-ra - %3$s \%% + Atera argazkia + Atera bideoa + Erabiltzailea + %1$s(r)en bideo grabazioa + Hizketaldia hemendik grabatzen %1$s (%2$s) + Sakatu grabatzeko, askatu bidaltzeko. + Audioa grabatzeko baimena behar da + « Irristatu bertan behera uzteko + Web-mintegia + Bai + Hurrengo astea + Ezin izan da telefono zenbakia integratu, baimen falta dela eta + Off + Lehenetsia + Ordu 1 + Linean + Lineako egoera + Ireki elkarrizketak + Ireki Fitxategiak aplikazioan + Erreproduzitu/pausatu ahots mezua + Gehitu aukera + Editatu botoa + Amaitu galdeketa + Ziur zaude galdeketa hau bukatu nahi duzula? Ezingo da atzera egin. + Ezin duzu aukera gehiagorekin bozkatu galdeketa honetan. + Erantzun anitz + Aukerak + Galdeketa pribatua + Galdera + Zure galdera + Emaitzak + Ezarpenak + Bozkatu + Botoa eman da + Lehendik ezarrita + Jaso eskua + Denak + Ezin dira fitxategiak biltegitik partekatu baimenik gabe + Deia grabatzen ari da + Utzi grabazioaren hasiera + Grabazioak huts egin du. Jarri zure administratzailearekin harremanetan. + Hasi grabatzen + Ziur zaude gelditu nahi duzula grabaketa? + Gelditu grabaketa + Utzi grabaketa + Grabazioa gelditzen… + Dei guztietan grabatzeko baimena behar da + Baliteke grabazioan zure ahotsa, kamerako bideoa eta pantaila partekatzea. Zure baimena beharrezkoa da deian sartu aurretik. Onartzen al duzu? + Eskatu grabatzeko baimena elkarrizketa honetako deian sartu aurretik + Grabatzeko baimena + Baliteke deia grabatzea. + Grabatzea + %1$s elkarrizketa gogokoetatik kendu da + %1$s elkarrizketa kendu da + Berrezarri egoera + Ezin da beste geletan sartu dei batean ari zaren bitartean + Gorde + Scan QR Code + Sinkronizatu zerbitzari fidagarriekin soilik + Federatua + Soilik instantzia honetako erabiltzaile eta gonbidatuentzat ikusgai + Lokala + Ikusgarri soilik mugikorrean Talk bidez telefono zenbaki integrazioarekin bat egin duten pertsonentzat + Pribatua + Sinkronizatu zerbitzari fidagarriekin eta helbide liburu publiko eta globalarekin + Argitaratuta + Irismen etengailua + Aldatu %1$s(r)en pribatutasun maila + Korritu beherantz + segundu lehenago + Hautatua + Bidali mezu elektronikoa + Bidali honi: + Ez duzu edukirik partekatzeko baimentik txat honetan + Bidali honi … + Bidali jakinarazpenik gabe + Ezarri + Ezarri avatarra kameraren bidez + Ezarri egoera + Ezarri egoera-mezua + Partekatu + Audioa + Fitxategia + Media + Bestelakoa + Galdeketa + Deia grabatzea + Ahotsa + Gogokoa + Ez duzu dei bat hasteko baimenik + dei bat hasi da + Egoera-mezua + Aldatu azpitaldearen gelara + Aldatu gela nagusira + Atera argazki bat + Errorea argazkia ateratzen + Ezin dira argazkiak atera baimenik gabe + Atera argazkia berriro + Bidali + Aldatu kamera + Moztu argazkia + Murriztu irudiaren tamaina + Txandakatu linterna + 30 minutu + Aste honetan + Hau probako mezu bat da + Asteburu hau + Gaur + Bihar + Itzuli + Itzulpena + Kopiatu itzulitako testua + Detektatu hizkuntza + Gailuaren ezarpenak + Ezin izan da hizkuntza hauteman + Itzulpenak huts egin du + Nork + Nori + eta beste 1 idazten ari dira ... + idazten ari dira ... + idazten ari da ... + eta beste %1$s idazten ari dira ... + Desartxibatu elkarrizketa + Behin elkarrizketa bat desartxibatuta, modu lehenetsian erakutsiko da berriro. + Debekua kendu + Irakurri gabe + Igo avatar berria gailutik + Ordezkoa: + Erabltzaile-avatarra + Helbidea + Izen osoa + E-posta + Telefono zenbakia + Twitter + Webgunea + Egoera + Erabiltzailearen informazio pertsonala eskuratzeak huts egin du + Ez da informazio pertsonalik ezarri + Gehitu izena, irudia eta kontaktuko xehetasunak zure profilaren orrian + Bideo deia + Zein da zure egoera? + + Boto %d + %d boto + + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000..e7aabaa --- /dev/null +++ b/app/src/main/res/values-fa/strings.xml @@ -0,0 +1,433 @@ + + + ویرایش + افزودن + افزودن به یادداشت‌ها + گفتگوی %1$sبه علاقه‌مندی‌ها افزوده شد + جستجو در %s + نمایش آفلاین + Archived + بلوتوث + خروجی صدا + شماره تلفن + بلندگو + گوشکی سیمی + Your status was set automatically + آواتار + دور + دکمهٔ بازگشت + انسداد + مسدود کردن شرکت‌کننده + فهرست مسدودی‌ها + مشغول + تقویم + گزینه‌های پیشرفتهٔ تماس + پیام وضعیت را پاک کن + بعد از آن پیام وضعیت را پاک کن + بسته + اتصال برقرار شد + گفتگو + ایجاد گفتگو + Custom + Danger zone + آواتار را پاک کن + مزاحم نشوید + پاک نکن + ویرایش + Edit message + اخیر + Encrypted + End call for everyone + ۱۵ دقیقه + پوشه + بارگذاری … + %1$s (%2$d) + ۴ ساعت + نامرئی + تا آخر وقت همین امروز + Leave call + بار کردن نتیحه‌های بیش‌تر + زمان محلی: %1$s + Lock conversation + نماد قفل + Mentioned + تازه‌ترین‌ها اول + قدیمی‌ترین‌ها اول + الف تا ی + ی تا الف + بزرگترین اول + کوچک‌ترین اول + جستجو نتیجه‌ای نداشت + جستجو ... + پیام ها + خاموش کردن همه اعلانات + حساب انتخاب‌شده اکنون وارد شده و در دسترس است . + درباره + کاربر فعال + افزودن حساب کاربری + حساب برای حذف برنامه ریزی شده است و قابل تغییر نیست + باز کردن منوی اصلی + افزودن پیوست + افزودن صورتک + افزودن به مکالمه + افزودن عضو + افزودن‌ به مورد علاقه‌ها + خوب ، همه انجام شد! + Pin: %1$s + باز کردن %1$s + پاسخ با تماس دیداری + پاسخ صرفا به صورت تماس شنیداری + تغییر خروجی صدا + تعویض دوربین + قطع تماس + Toggle microphone + Open picture-in-picture mode + تغییر به ویدئوی خود + INCOMING + نام مکالمه + Reconnecting … + RINGING + %1$s with phone + در ۴۵ ثانیه پاسخی وجود نداشت ، برای آزمایش دوباره ضربه بزنید + لغو + دریافت قابلیت‌ها موفقیت‌آمیز نبود، درحال لغو + آیا به گواهی SSL ناشناخته ، صادر شده توسط ، اعتماد دارید %1$s برای %2$s, معتبر از %3$s به %4$s؟ + گواهی را بررسی کنید + راه اندازی SSL شما مانع از اتصال شد + گواهی تأیید اعتبار را تغییر دهید + تغییر گذرواژه + Cancel editing + Cancel editing + حذف تمام پیام‌ها + تمام پیام‌ها حذف شدند + آیا واقعا می‌خواهید تمام پیام‌ها را در این مکالمه حذف کنید؟ + تغییر گواهی مشتری + گواهی مشتری تنظیم کنید + و + رونوشت + در حافظه رونویسی شد + ایجاد + غیرفعال شده + پنهان کن + متاسفیم؛ خطایی پیش آمد + More options + تنظیم + پرش + ناشناخته + گواهی تأیید اعتبار را انتخاب کنید + Connecting … + انجام شد + اطلاعات مکالمه + تماس تصویری + تماس صوتی + تنظیمات گفتگو + عضو یک گفتگو شوید یا یک گفتگو جدید شروع کنید + به رفقا و هم‌قطاران‌تان سلام کنید! + کپی کردن + امروز + دیروز + حذف + همه را حذف کنید + مکالمه را حذف کنید + اگر این مکالمه را حذف کنید ، برای سایر شرکت کنندگان نیز حذف خواهد شد. + Delete message + تنزل مقام از مدیر + ضبط پیام صوتی + فرستادن پیام + حساب جاری + سرور + کاربر + نسخه اندروید + کاره + نام کاره + Registered users + تنظیمات باتری + افزاره + تلفن + خارجی + داخلی + گذرواژه نادرست + برنامه، قدیمی شده است + به‌روز رسانی + خیر + بله + امکان دریافت نام وجود ندارد. درحال لغو + "امکان ذخیره نام وجود ندارد، درحال لغو " + ویرایش + ویرایش + Edit message + 8 ساعت + 4 weeks + خاموش + 1 روز + ۱ ساعت + 1 week + " دریافت تنظیمات سیگنال نا‌موفق بود" + تایید + رد کردن + بازگشت + کاربر پس از یک پیوند عمومی + شما: %1$s + ارسال کردن + ارسال کردن به … + گالری + "آیا هیچ سروری در دسترس ندارید؟ برای گرفتن سرور از یک ارائه دهنده کلیک کنید " + دریافت کد منبع + گروه + مهمان + اجازه میهمانان + یک گذرواژه وارد کنید + محافظت از رمز عبور + گفتگوی مهم + دعوت‌ها + پیوستن به گفتگوهای باز + نگاه داشتن + You need to promote a new moderator before you can leave the conversation + تغییرات %1$s | آخرین تغییرات: %2$s + ترک گفتگو + در حال ترک تماس ... + مجوز عمومی عمومی گنو، نسخه ۳ + مجوز + به حد بالای %s نویسه رسیدید + لابی + شما در حال حاضر در لابی منتظر هستید. + مکان فعلی شما + دسترسی مکان مورد نیاز است + موقعیت ناشناخته + قفل‌شده + برای باز کردن قفل ضربه بزنید + تنظیم نشده + علامت به عنوان خوانده‌شده + علامت به عنوان خوانده‌نشده + Failed + ناتوانی در فرستادن پیام: + آفلاین + لغو پاسخ + پیام، خوانده شد + پیام، ارسال شد + شما یک تماس از %s را از دست دادید + مدیر + گفتگوی جدید + Visibility + پیام‌های خوانده نشده + مهمان + نه + گفتگوی بازی وجود ندارد + پروکسی نیست + مجاز به فعال کردن ویدئو نیستید! + Not now + کانال اعلان %1$s روی %2$s + تماس‌ها + پیام‌ها + بارگذاری‌ها + آگاهی‌رسانی دربارهٔ پیشرفت بارگذاری + تنظیمات اعلان + همیشه اعلان شود + وقتی ذکر شد اطلاع دهید + هرگزاعلان نشود + در حال حاضر آفلاین است ، لطفا اتصال خود را بررسی کنید + تایید + Also open to guest app users + مالک + شركت كنندگان + افزودن شرکت‌کننده + گذرواژه + تنظیم دسترسی‌ها + تنظیمات را باز کنید + حساب یافت نشد + گپ از راه %s + بی‌صدا کردن میکروفن + فعال کردن میکروفن + پیام‌ها + حریم خصوصی + مشخصات شخصی + ترفیع به مدیر + دریافت اعلانات غیرفعال است + برای صحبت کردن فشار دهید + با غیر فعال کردن میکروفن، برای استفاده از Push-to-talk کلیک & نگه‌داشتن را انجام دهید + بعدا به من یادآوری کن + حذف کردن از مورد علاقه‌ها + حذف گروه و اعضا + شرکت کننده را حذف کنید + تغییر نام مکالمه + تغییرنام + پاسخ + پاسخ خصوصی + ذخیره + ۳۰ ثانیه + ۵ دقیقه + ۱ دقیقه + ۱۰ دقیقه + ۶۰۰ + ۶۰ + ۳۰ + ۳۰۰ + جستجو + پاک کردن جستجو. + یک حساب کاربری انتخاب کنید + %1$s یک گیف فرستاده. + شما یک GIF ارسال کردید. + %1$s یک ویدئو فرستاد. + شما یک ویدیو ارسال کردید + %1$s صدایی فرستاد. + شما صوتی ارسال کردید. + %1$s تصویری فرستاد. + شما یک تصویر ارسال کردید + آزمایش اتصال به سرور + لطفا پایگاه داده %1$s خود را ارتقا دهید + " وارد کردن حساب انتخاب‌شده موفقیت آمیز نبود" + پیوند به رابط وب %1$s شما زمانی که در مرورگر بازش می‌کنید. + " وارد کردن حساب از برنامه %1$s" + وارد کردن حساب + " وارد کردن حساب ها از برنامه %1$s" + " وارد کردن حساب‌ها" + لطفا نصب %1$s را به پایان برسانید. + اجرای تست ارتباط + کارگزار برنامه صحبت را پشتیبانی نمی‌کند + نشانی سرور ‪https://… + سرور %1$s فقط با %2$s و بالاتر کار می‌کند + تنظیمات + به جای اضافه کردن حساب جدید، حساب از قبل موجود شما بروز شد + پیشرفته + ظاهر + Calls + دستورالعمل صفحه کلید را برای غیرفعال کردن یادگیری شخصی (بدون ضمانت) + صفحه کلید ناشناس + بدون صدا + برنامه Talk روی سروری که قصد اعتبار سنجی در آن را دارید، نصب نشده است + آگاهی‌ها + پیام ها + شماره تلفن را وارد کنید + شما تلفن نامعتبر + شمارهٔ تلفن با موفقیت تنظیم شد + شماره تلفن + حریم خصوصی + میزبان پروکسی + گذرواژهٔ پروکسی + درگاه پروکسی + نوع پروکسی + نام کاربری پروکسی + حذف + حذف حساب کاربری + لطفاً قصد خود را برای حذف حساب جاری تأیید کنید. + قفل %1$s با قفل صفحه Android یا روش بیومتریک پشتیبانی شده + زمان غیرفعال کردن قفل صفحه + قفل صفحه + از گرفتن عکسهای صفحه در لیست اخیر و داخل برنامه جلوگیری می کند + امنیت صفحه نمایش + تیره + از پیش‌فرض سیستم استفاده کنید + پوسته + روشن + پوسته + هشدار + فقط حساب کاربری فعلی قابل گرفتن مجوز مجدد است + هم‌رسانی مخاطب + مجوز دسترسی برای خواندن مخاطبان مورد نیاز است + هم‌رسانی مکان فعلی + هم‌رسانی پیوند + هم‌رسانی مکان + هم‌رسانی این مکان + حساب کاربری را انتخاب کنید + موارد هم‌رسانی‌شده + کارت deck + مکان + مکان هم‌رسانی‌شده + مرتب‌سازی بر اساس + زمان شروع + تعویض حساب + تیم + انتخاب پرونده‌ها + متاسفانه بارگذاری شکست خورد + ناتوانی در بارگذاری %1$s + شکست + هم‌رسانی از %1$s + بارگذاری از دستگاه + در حال بارگذاری + عکس گرفتن + ضبط ویدئو + کاربر + برای ضبط نگه‌داشته و برای فرستادن، رها کنید. + جواز دسترسی به ضبط صدا مورد نیاز است + وبینار + بله + هفتهٔ بعد + All messages + \@-mentions only + پیش‌فرض + ۱ ساعت + برخط + وضعیت برخط + گفتگوی جدید + پخش/مکث پیام شنیداری + افزودن گزینه + گزینه‌ها + نتایج + تنظیمات + رای + رای فرستاده شد + Previously set + بالا بردن دست + همه + ضبط جلسه + Reset status + ذخیره + Scan QR Code + فقط با سرورهای قابل اعتماد همگام سازی شود + فدرال + فقط برای کاربران این نسخه و مهمانان قابل مشاهده است + محلی + فقط برای کاربرانی فعال است که از طریق یکپارچه سازی شماره تلفن در برنامه Talk روی تلفن همراه مطابقت داشته باشند + خصوصی + با سرورهای قابل اعتماد و دفترچه آدرس عمومی و همگانی همگام سازی شود + منتشر شده + یک ثانیه پیش + انتخاب شد + فرستادن رایانامه + فرستادن به + تنظیم + تنظیم وضعیت + تنظیم پیام وضعیت + هم‌رسانی + صدا + پرونده + رسانه‌ها + اعلان‌ها + Poll + ضبط تماس + صدا + مورد‌ ‌علاقه‌ + پیغام وضعیت + عکس گرفتن + ارسال + دوربین را عوض کنید + تغییر مشعل + ۳۰ دقیقه + این هفته + این آخر هفته + امروز + فردا + ترجمه + ترجمه + رونوشت متن ترجمه + تشخیص زبان + تنظیمات افزاره + امکان تشخیص زبان وجود ندارد. + از + به + آواتار کاربر + نشانی + نام کامل + رایانامه + شماره تلفن + توییتر + وب‌ سایت + وضعیت + اطلاعات شخصی تنطیم نشده + نام، تصویر و اطلاعات تماس را در صفحه نمایه خود اضافه کنید. + وضعیت شما چیست؟ + + %d vote + %d رای + + diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml new file mode 100644 index 0000000..baab44e --- /dev/null +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -0,0 +1,438 @@ + + + Muokkaa + Lisää + Etsi kohteesta %s + Näytä olevan poissa + Arkistoitu + Bluetooth + Äänen ulostulo + Puhelin + Kaiutin + Langallinen kuulokemikrofoni + Tilatietosi asetettiin automaattisesti + Profiilikuva + Poissa + Varattu + Kalenteri + Tämä puhelu on kestänyt yhden tunnin. + Puhelu ilman ilmoitusta + Kameraoikeus myönnetty. Valitse kamera uudelleen. + Peru kirjautuminen + Tyhjennä tilaviesti + Tyhjennä tilaviesti, kun on kulunut + Sulje + Yhteys muodostettu + Keskustelut + Luo keskustelu + Omavalintainen + Vaaravyöhyke + Älä häiritse + Älä tyhjennä + Muokkaa + Muokkaa viestiä + Viimeaikaiset + Salattu + Lopeta puhelu + Päätä puhelu kaikkien osalta + Ei voitu tallentaa %1$s + 15 minuuttia + kansio + Ladataan… + %1$s (%2$d) + 4 tuntia + (muokattu) + Näkymätön + Myöhemmin tänään + Poistu puhelusta + Lataa lisää tuloksia + Lukitse keskustelu + Laske käsi + Uusin ensin + Vanhin ensin + A - Ö + Ö - A + Suurin ensin + Pienin ensin + Ei hakutuloksia + Aloita kirjoittaminen hakeaksesi… + Hae… + Viestit + Mykistä kaikki ilmoitukset + Valittu tili on nyt tuotu ja käytettävissä + Tietoja + Aktiivinen käyttäjä + Lisää tili + Tili on aikataulutettu poistettavaksi, joten tiliä ei voi muuttaa + Avaa päävalikko + Lisää liite + Lisää emoji + Lisää keskusteluun + Lisää osallistujat + Lisää suosikkeihin + OK, kaikki valmista! + Pin: %1$s + Avaa lukitus %1$s + Lopeta + SAAPUVA + Keskustelun nimi + Puheluilmoitukset + Yhdistetään uudelleen … + SOITETAAN + Ei vastausta 45 sekunnissa, napauta yrittääksesi uudelleen + Peruuta + Tarkista varmenne + SSL-asetukset esti yhteyden + Vaihda tunnistautumisvarmenne + Vaihda salasana + Peruuta muokkaus + Peruuta muokkaus + Poista kaikki viestit + Kaikki viestit poistettiin + Haluatko varmasti poistaa kaikki viestit tästä keskustelusta? + Vaihda asiakasvarmenne + Aseta asiakasvarmenne + ja + Kopioi + Kopioitu leikepöydälle + Luo + Pois käytöstä + Hylkää + Pahoittelut, jokin meni vikaan! + Aseta + Ohita + Tuntematon + Valitse tunnistautumisvarmenne + Yhdistetään… + Valmis + Keskustelun tiedot + Videopuhelu + Äänipuhelu + Keskustelua ei löydy + Keskustelun asetukset + Liity keskusteluun tai aloita uusi + Sano hei ystävillesi ja työkavereillesi! + Kopioi + Luo uusi keskustelu + Luo kysely + Tänään + Eilen + Poista + Poista kaikki + Poista keskustelu + Jos poistat keskustelun, keskustelu poistetaan myös muilta osapuolilta. + Poista viesti + Alenna moderaattorista + Äänitä ääniviesti + Lähetä viesti + Nykyinen tili + Palvelin + Käyttäjä + Android-versio + Sovellus + Sovelluksen nimi + Rekisteröityneet käyttäjät + Akkuasetukset + Laite + Puhelin + Ulkoinen + Sisäinen + Virheellinen salasana + Päivitä + Haluatko valtuuttaa tämän tilin uudelleen vai poistaa tilin? + Ei + Kyllä + Muokkaa + Muokkaa + Muokkaa viestiä + 8 tuntia + 4 viikkoa + Pois + 1 päivä + 1 tunti + 1 viikko + Keskusteluviestit on mahdollista vanhentaa tietyn ajan jälkeen. Huomio: Keskustelussa jaetut tiedostot eivät poistu omistajalta, mutta ne eivät ole enää jaettu keskustelussa. + Hyväksy + Hylkää + Ei odottavia kutsuja + Takaisin + Sinä: %1$s + Välitä + Välitä viesti … + Galleria + Ei palvelinta?\nNapsauta tästä ja hanki palveluntarjoajalta + Lataa lähdekoodi + Ryhmä + Vieras + Vieraspääsy + Salli vieraat + Syötä salasana + Aseta salasana rajoittaaksesi, ketkä voivat käyttää julkista linkkiä. + Salasanasuojaus + Lähetä kutsut uudelleen + Kutsut lähetettiin uudelleen. + Jaa keskustelulinkki + Kirjoita viesti… + Tärkeä keskustelu + Kutsut + Liity avoimiin keskusteluihin + %1$s | Viimeksi muokattu: %2$s + Poistu keskustelusta + Poistutaan puhelusta… + GNU yleinen lisenssi, versio 3 + Lisenssi + %s merkin raja tuli täyteen + Aula + Kokous on ajoitettu alkavaksi %1$s + Kokous alkaa pian + Odotat parhaillaan aulassa. + Nykyinen sijaintisi + sijainnin käyttöoikeus vaaditaan + Tuntematon sijainti + Lukittu + Napauta avataksesi lukituksen + Ei asetettu + Merkitse luetuksi + Merkitse lukemattomaksi + Epäonnistui + Viestin lähettäminen epäonnistui: + Poissa + Peruuta vastaus + Viesti luettu + Viesti lähetetty + Moderaattori + Uusi keskustelu + Näkyvyys + Lukemattomat maininnat + Lukemattomat viestit + Vieras + Ei + Ei välityspalvelinta + Ei nyt + Puhelut + Ilmoita saapuvista puheluista + Viestit + Ilmoita saapuvista viesteistä + Lähetykset + Ilmoitusasetukset + Ilmoita aina + Ilmoita kun mainittu + Älä ilmoita koskaan + Ei yhteyttä tällä hetkellä, tarkista verkkoyhteydet + OK + Avaa keskustelu rekisteröityneille käyttäjille + Omistaja + Osallistujat + Lisää osallistujat + Salasana + Avaa asetukset + Tiliä ei löydy + Mykistä mikrofoni + Käytä mikrofonia + Viestit + Yksityisyys + Henkilökohtaiset tiedot + Ylennä moderaattoriksi + Julkinen keskustelu + Push-ilmoitukset pois käytöstä + Paina puhuaksesi + Muistuta myöhemmin + Poista suosikeista + Poista ryhmä ja jäsenet + Poista osallistuja + Poista tiimi ja jäsenet + Nimeä keskustelu uudelleen + Nimeä uudelleen + Vastaa + Vastaa yksityisesti + Tallenna + Tallennettu onnistuneesti + 30 sekuntia + 5 minuuttia + 1 minuutti + 10 minuuttia + 600 + 60 + 30 + 300 + Etsi + Tyhjennä haku + Valitse tili + %1$s lähetti GIF-kuvan. + Lähetit GIF-kuvan. + %1$s lähetti videon. + Lähetit videon. + %1$s lähetti ääntä. + Lähetit ääntä. + %1$s lähetti kuvan. + Lähetit kuvan. + Testaa palvelinyhteys + Päivitä %1$s-tietokantasi + Valitun tilin tuominen epäonnistui + Linkki %1$s selainkäyttöliittymääsi. + Tuo tili + Tuo tilejä + Poista %1$s huoltotilasta + Viimeistele %1$s-asennuksesi + Testataan yhteyttä + Tuettua Talk-sovellusta ei ole asennettu palvelimelle + Palvelimen osoite https://… + Aseta uusi salasana: + Asetukset + Olemassa oleva tilisi päivitettiin sen sijaan, että olisi lisätty tili + Lisäasetukset + Ulkoasu + Puhelut + Incognito-näppäimistö + Ei ääntä + Talk-sovellusta ei ole asennettu palvelimelle, johon koetit kirjautua + Ilmoitukset + Viestit + Kirjoita puhelinnumero + Virheellinen puhelinnumero + Puhelinnumero asetettu onnistuneesti + Puhelinnumero + Puhelinnumeron integraatio + Yksityisyys + Välityspalvelimen osoite + Välityspalvelimen salasana + Välityspalvelimen portti + Välityspalvelimen tyyppi + Välityspalvelimen käyttäjä + Jaa lukukuittaukseni ja näytä muiden lukukuittaukset + Lue tila + Valtuuta tili uudelleen + Poista + Poista tili + Vahvista nykyisen tilin poistaminen. + Näytön käyttämättömyyden ajastinlukitus + Näytönlukitus + Estää näyttökuvat viimeisimmät listassa sovelluksen sisällä + Näytön turvallisuus + Palvelin ei ole tuettu + Tumma + Käytä järjestelmän oletusta + teema + Vaalea + Teema + Välityspalvelin vaatii kirjautumistiedot + Varoitus + Vain nykyinen tili voidaan valtuuttaa uudelleen + Oikeus lukea yhteystiedot vaaditaan + Jaa nykyinen sijainti + Jaa linkki + Jaa sijainti + Jaa tämä sijainti + Valitse tili + Jaetut tietueet + Deck-kortti + Kuvat, tiedostot, ääniviestit… + Ei jaettuja kohteita + Sijainti + Jaettu sijainti + Lajittelujärjestys + Aloitusaika + Vaihda tiliä + Tiimi + Valitse tiedostot + Lähetys epäonnistui + Lähetä laitteelta + Lähetys + Ota kuva + Käyttäjä + Oikeus äänen tallentamiseen vaaditaan + « Liu\'uta peruuttaaksesi + Webinaari + Kyllä + Seuraava viikko + Oletus + 1 tunti + Paikalla + Online-tila + Avaa keskustelut + Toista/keskeytä ääniviesti + Lisää valinta + Muokkaa ääntä + Lopeta kysely + Haluatko varmasti lopettaa kyselyn? Toimintoa ei voi perua. + Useita vastauksia + Valinnat + Yksityinen kysely + Kysymys + Tulokset + Asetukset + Äänestä + Aiemmin asetettu + Nosta käsi + Kaikki + Tallennus epäonnistui. Ole yhteydessä ylläpitoon. + Käynnistä tallennus + Lopeta tallennus + Palauta tilatieto + Tallenna + Skannaa QR-koodi + Synkronoi vain luotetuille palveluille + Federoitu + Vain näkyvissä tämän instanssin ihmisille ja vieraille + Paikallinen + Yksityinen + Julkaistu + Vieritä alas + sekunteja sitten + Valittu + Lähetä sähköposti + Sinulla ei ole oikeutta jakaa sisältöä tähän keskusteluun + Lähetä… + Lähetä ilman ilmoitusta + Aseta + Aseta tilatieto + Aseta tilaviesti + Jaa + Ääni + Tiedosto + Media + Muu + Kysely + Ääni + Suosikki + Sinulla ei ole oikeutta aloittaa puhelua + Tilaviesti + Ota kuva + Virhe kuvaa ottaessa + Kuvan ottaminen ei ole mahdollista ilman käyttöoikeuksia + Ota kuva uudelleen + Lähetä + Vaihda kamera + Rajaa kuvaa + Pienennä kuvan kokoa + 30 minuuttia + Tämä viikko + Tämä viikonloppu + Tänään + Huomenna + Käännä + Kopioi käännetty teksti + Havaitse kieli + Laiteasetukset + Kielen havaitseminen ei onnistunut + Kääntäminen epäonnistui + Lähettäjä + Vastaanottaja + Lukematon + Käyttäjän avatar + Osoite + Koko nimi + Sähköposti + Puhelinnumero + Twitter + Verkkosivu + Tila + Henkilökohtaisten tietojen noutaminen epäonnistui. + Henkilökohtaisia tietoja ei ole asetettu + Lisää nimi, kuva ja yhteystiedot profiilisivullasi. + Mikä on tilatietosi? + + %d ääni + %d ääntä + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..5cebfd8 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,727 @@ + + + Modifier + Ajouter + Ajouter à Notes + La conversation %1$s a été ajoutée aux favoris + Rechercher dans %s + Apparaître hors-ligne + Archiver la conversation + Lorsqu\'une conversation est archivée, elle est cachée par défaut. Choisissez le filtre \"Archivés\" pour voir les conversations archivées. Les mentions directes seront toujours bien reçues. + Archivé + %1$s archivée + Appel audio + Bluetooth + Sortie audio + Téléphone + Haut-parleur + Écouteurs filaires + Votre statut a été automatiquement défini + Avatar + Absent(e) + Bouton précédent + Bannir + Bannir le participant + Liste des bannis + Occupé + Agenda + Options d\'appel avancées + L\'appel est en cours depuis une heure. + Appeler sans notification + Caméra autorisée. Merci de choisir à nouveau la caméra. + Annuler la connexion + Sélectionner l\'avatar depuis le cloud + Effacer le message d\'état + Effacer le message d\'état après + Fermer + Icône de fermeture + Connexion établie + Aucune connexion au serveur + Connexion perdue - les messages envoyés sont mis en file d\'attente + Verrouiller l\'enregistrement pour enregistrer en continu le message vocal + La conversation est archivée + La conversation est en lecture seule + Impossible de passer la conversation en lecture seule + Discussions + Créer la conversation + Signaler un problème + Personnalisé + Zone de danger + %1$s dans %2$s + Supprimer l\'avatar + Supprimer l\'enregistrement vocal + La conversation %1$s a été supprimée + Ne pas déranger + Ne pas effacer + Modifier + Les messages datant de plus de 24 heures ne peuvent pas être modifiés + Modifier le message + Récent + Chiffré + Terminer l\'appel + Mettre fin à l\'appel pour tout le monde + Un problème est survenu lors du chargement de vos discussions + Une erreur est survenue à la réintégration du participant + Échec de la sauvegarde de %1$s + 15 minutes + Dossier + Chargement… + %1$s (%2$d) + Conversations suivies + 4 heures + Échec de la récupération des invitations en attente + (modifié) + Note interne + Invisible + Les langues n\'ont pas pu être récupérées + Récupération échouée + Plus tard aujourd\'hui + Quitter l\'appel + Vous avez quitté la conversation %1$s + Charger plus de résultats + Heure locale : %1$s + Autorisation de localisation refusée + Services de localisation désactivés + Verrouiller la conversation + Symbole de verrouillage + Baisser la main + La conversation %1$s a été marquée comme lue + La conversation %1$s a été marquée comme non-lue + Mentionné + Les plus récents d\'abord + Les plus anciens d\'abord + A - Z + Z - A + Les plus gros d\'abord + Le plus petit en premier + Message copié + Êtes-vous sûr de vouloir effacer ce message ? + Message supprimé par vous + Modifié par %1$s + Toucher pour ouvrir le sondage + Aucun résultat de recherche + Commencer à taper pour lancer la recherche… + Recherche… + Messages + Désactiver les notifications + Le compte sélectionné est maintenant importé et disponible + À propos + Utilisateur actif + Ajouter un compte + La suppression du compte est planifiée, il ne peut donc pas être modifié + Ouvrir le menu principal + Ajouter une pièce jointe + Ajouter des emojis + Ajouter à la conversation + Ajouter des participants + Ajouter aux favoris + OK, tout est fini ! + Épingler : %1$s + Déverrouiller %1$s + Pour activer les écouteurs Bluetooth, merci de donner la permission « Appareils à proximité ». + Répondre par appel vidéo + Répondre par appel vocal uniquement + Changer de sortie audio + Activer / Désactiver la caméra + Raccrocher + Activer / Désactiver le micro + Lancer le mode Image dans l\'Image + Basculer mode selfie + ENTRANT + Nom de la conversation + Notifications d\'appel + %1$s a levé la main + Reconnexion… + APPEL EN COURS + %1$s en ligne + %1$s avec un téléphone + %1$s avec la vidéo + Aucune réponse depuis 45 secondes, appuyez pour essayer à nouveau + %s appel + %s appel vidéo + %s appel audio + Pour activer la communication vidéo, merci de donner l\'autorisation « Appareil photo ». + Annuler + Échec de la récupération des capacités, abandon + Légende + Faites-vous confiance au certificat SSL jusque-là inconnu, émis par %1$s pour %2$s, et et valide du %3$s au %4$s ? + Vérifier le certificat + Votre configuration SSL a empêché la connexion + Modifier le certificat d\'authentification + Changer son mot de passe + Annuler la modification + Annuler la modification + Supprimer tous les messages + Tous les messages ont été supprimés + Voulez-vous vraiment supprimer tous les messages de cette conversation ? + Modifier le certificat du client + Configurer le certificat du client + et + Copier + Copié dans le presse-papier + Créer + Désactivé + Ignorer + Désolé, quelque chose s’est mal passé ! + Plus d\'options + Sauver + Ignorer + Inconnu + Sélectionner le certificat d\'authentification + Connexion… + Terminé + Description de conversation + Infos sur la conversation + Appel vidéo + Appel vocal + Discussion non trouvée + Paramètres de la conversation + Rejoignez une conversation ou lancez-en une nouvelle + Dites bonjour à vos amis et collègues ! + Copier + Créer une nouvelle conversation + Créer un sondage + Toi : + Aujourd\'hui + Hier + Supprimer + Supprimer tout + Supprimer la conversation + Si vous supprimez la conversation, elle sera supprimée pour tous les participants. + Supprimer le message + Message supprimé avec succès, mais il pourrait avoir été divulgué à d’autres services + Supprimer maintenant + L\'utilisateur %1$s a été supprimé + Retirer le statut de modérateur + Enregistrer un message vocal + Envoyer un message + Compte actuel + Serveur + Application de notification serveur installée ? + Utilisateur + Statut de l\'utilisateur activé ? + Version Android + App + Nom de l’application + Utilisateurs enregistrés + Version de l\'app + Optimisation de la batterie ignorée, tout va bien + L\'optimisation de la batterie est activée ce qui peut causer des problèmes. Vous devriez désactiver l\'optimisation de la batterie ! + Paramètres pour la batterie + Appareil + Ouvrir la liste de vérifications des erreurs communes + Ouvrir l\'écran de diagnostic + Ouvrir dontkillmyapp.com + Récupération du dernier token push firebase + Dernière génération de token push firebase + Aucun jeton de poussée Firebase définit. Veuillez créer un rapport de bogue. + Jeton de poussé Firebase + Les services Google Play ne sont pas disponibles. Les notifications ne sont pas prises en charge + Services Google Play + Les services Google Play sont disponibles + Dernière inscription push sur proxy push + Pas encore inscrit sur le proxy push + Dernière inscription push sur le serveur + Pas encore inscrit sur le serveur + Méta-informations + Génération du rapport système + Canal de notification des appels activé ? + Canal de notification des messages activé ? + Permissions de notification + Téléphone + Version du serveur Talk + Version du serveur + Externe + Interne + Mode de signalement + Mot de passe incorrect + Le serveur est actuellement en mode maintenance. + L\'application est obsolète + L\'application est trop ancienne et n\'est plus prise en charge par ce serveur. Veuillez la mettre à jour. + Mise à jour + Voulez-vous autoriser à nouveau ou supprimer ce compte ? + Sauvegarder ce média autorisera toutes les autres applications de votre appareil à y accéder. + Poursuivre ? + Non + Sauvegarder ? + Oui + Le nom d’affichage n’a pas pu être récupéré, annulation + Impossible d\'enregistrer le nom d\'affichage, abandon + Éditer + Modifier + Modifier le message + Modifié par un administrateur + Menu de conversation d\'événement + Calendrier + 8 heures + 4 semaines + Éteint + 1 jour + 1 heure + 1 semaine + Expiration pour les messages + Les messages instantanés peuvent expirer après un certain temps. Note : les fichiers partagés dans la discussion ne seront pas effacé pour leur propriétaire, mais ils ne seront plus partagés dans la conversation. + Échec de l\'extraction des paramètres de signalisation + Accepter + Rejeter + de %1$s à %2$s + Aucune invitation en attente + Vous avez des invitations en attente + Retour + La permission d\'accès aux fichiers est requise + Filtrer les conversations + Utilisateur suivant un lien public + Vous : %1$s + Transférer + Transférer à... + Galerie + Vous n\'avez pas encore de serveur ?\nCliquez-ici pour en obtenir un chez un fournisseur. + Obtenir le code source + Groupe + Invité + Accès invité + Impossible d\'activer / désactiver l\'accès invité. + Autoriser les inviter à partager le lien public pour rejoindre cette conversation + Autoriser les invités + Entrez un mot de passe + Mot de passe de l\'accès invité + Erreur pendant le paramétrage ou la désactivation du mot de passe. + Définissez un mot de passe pour restreindre l\'usage du lien public + Protection par mot de passe + Renvoyer les invitations + Les invitations n\'ont pas été envoyées en raison d\'une erreur. + Les invitations ont été envoyées à nouveau. + Partager le lien de conversation + Saisir un message… + L\'optimisation de la batterie n\'est pas ignorée. Ceci devrait être modifié pour vous assurer que les notifications fonctionnent en arrière-plan. Merci de cliquer OK et sélectionner \"Toutes les applications\" -> %1$s -> Ne pas optimiser + Ignorer l\'optimisation de batterie + Conversation importante + Le statut utilisateur \"Ne pas déranger\" est ignoré pour les conversations importantes + Heure invalide + Invitations + Rejoindre des conversations ouvertes + Conserver + Vous devez promouvoir un nouveau modérateur avant de pouvoir quitter la conversation. + %1$s | Dernière modification : %2$s + Quitter la conversation + Fin d\'appel… + Licence Publique Générale GNU, Version 3 + Licence + La limite de %s caractères a été atteinte + Salle d\'attente + La réunion est planifiée pour %1$s + La réunion va bientôt commencer + Vous patientez actuellement dans la salle d\'attente + Votre position actuelle + La permission d\'accès à votre position est requise + Position inconnue + Verrouillé + Appuyer pour déverrouiller + Non défini + Marquer comme lu + Marquer comme non lu + Conversation marquée comme importante + Conversation sensible désactivée + Conversation marquée comme sensible + Conversation importante désactivée + Réunion terminée + Message ajouté aux notes + Échec + Échec d\'envoi du message : + Hors-ligne + Annuler la réponse + Message lu + Envoi en cours + Message envoyé + Le micro est activé et le son est enregistré + Pour autoriser la communication audio, merci de donner l\'autorisation « Microphone ». + Vous avez manqué un appel de %s + Modérateur + Nouvelle conversation + Visibilité + Mentions non lues + Messages non lus + %1$s non disponible (non installé ou restreint par l\'administrateur) + Invité + Non + Pas de conversations ouvertes + Aucune conversation ouverte.\nSoit il n\'y aucune conversation ouverte, soit vous les avez déjà toutes rejointes. + Aucun proxy + Vous n\'êtes pas autorisé(e) à activer l\'audio ! + Vous n\'êtes pas autorisé(e) à activer la vidéo ! + Pas maintenant + Canal de notification %1$s sur %2$s + Appels + Notifier les appels entrants + Messages + Notifier les messages entrants + Envois + Notifier la progression du téléversement + Paramètres de notification + Les notifications ne sont pas configurées correctement + Les permissions de notification et les paramètres de la batterie sont correctement configurés pour recevoir les notifications. Si vous avez néanmoins des problèmes pour recevoir les notifications, merci de vérifier que les canaux de notification pour appels et messages sont activés. De l\'aide supplémentaire peut être trouvée sur DontKillMyApp.com ou dans la liste des vérifications des erreurs communes. Si cela ne fonctionne toujours pas, merci d\'aller sur l\'écran de diagnostic et envoyer un rapport de bug. + Erreurs communes au sujet de la notification + Toujours notifier + Notifier quand mentionné + Ne jamais notifier + Actuellement hors-ligne, veuillez vérifier votre connexion + OK + Réunion en cours + Ouvrir la conversation aux utilisateurs enregistrés + Également ouvert aux utilisateurs invités de l\'application + Propriétaire + Participants + Ajouter des participants + Mot de passe + Définir les autorisations + Certaines autorisations n\'ont pas été accordées. + Merci de donner les autorisations + Ouvrir les paramètres + Merci de donner les autorisations dans Paramètres > Autorisations + Compte introuvable + Chat via %s + Désactiver le micro + Activer le micro + Messages + Vie privée + Informations personnelles + Promouvoir comme modérateur + Conversation publique + Notifications push désactivées + Désolé, quelque chose s\'est mal passé, l\'erreur est %1$s + Désolé, quelque chose s\'est mal passé, impossible de récupérer le message de test push + La notification push a été envoyée avec succès. Vous devriez maintenant recevoir une notification sur cet appareil intitulée « Test des notifications push ». + Talkie-walkie + Avec le micro désactivé, cliquez & maintenez pour utiliser la fonction Talkie-walkie. + Rappelez-moi plus tard + Retirer des favoris + Supprimer un groupe et ses membres + Retirer le participant + Supprimer le Mot de passe + Supprimer l\'équipe et ses membres + Renommer la conversation + Renommer + Répondre + Répondre en privé + Salle réservée avec succès + Enregistrer + Enregistré avec succès + 30 secondes + 5 minutes + 1 minute + 10 minutes + Immédiat + 600 + 60 + 30 + 300 + Recherche + Effacer la recherche + Choisissez un compte + Message de mise à jour + Envoyer l\'enregistrement vocal + Conversation sensible + La prévisualisation des messages sera désactivée dans la liste des conversations et les notifiactions + %1$s a envoyé un GIF. + Vous avez envoyé un GIF. + %1$s a envoyé une vidéo. + Vous avez envoyé une vidéo. + %1$s a envoyé un fichier audio. + Vous avez envoyé un fichier audio. + %1$s a envoyé une image. + Vous avez envoyé une image. + %1$s a envoyé un deck de carte + Test de la connexion au serveur + Veuillez mettre à jour votre base de données %1$s + Échec lors de l\'importation des comptes sélectionnés + Adresse URL visible dans la barre d\'adresse de votre navigateur Web lorsque vous êtes connecté à %1$s. + Importer le compte depuis l\'application %1$s + Importer le compte + Importer les comptes depuis l\'application %1$s + Importer les comptes + S’il vous plaît, désactivez la maintenance de %1$s + Veuillez terminer votre installation de %1$s + Tester la connexion + Le serveur n\'a pas d\'application Talk prise en charge installée. + Adresse du serveur https://… + %1$s ne fonctionne qu\'avec %2$s 13 et supérieur + Définir un nouveau mot de passe + Définir le Mot de passe + Paramètres + Nous avons mis à jour votre compte existant au lieu d’en ajouter un nouveau + Avancé + Apparence + Appels + Merci de contacter l\'administrateur de + Ouvrir l\'écran de diagnostic pour vérifier les réglages ou créer un rapport de bug + Diagnostic + Indique au clavier de désactiver l\'apprentissage personnalisé (sans garantie) + Clavier incognito + Aucun son + L\'application Talk n\'est pas installée sur le serveur auquel vous essayez de vous connecter. + Notifications + Les notifications sont désactivées + Les notifications sont acceptées + Messages + Mettre en correspondance les contacts à l\'aide du numéro de téléphone pour intégrer des raccourcis Talk dans le carnet d\'adresses + Erreur 429 Trop de requêtes + Vous pouvez ajouter votre numéro de téléphone pour que les autres utilisateurs puissent vous trouver + Saisissez un numéro de téléphone + Numéro de téléphone invalide + Numéro de téléphone configuré avec succès + Numéro de téléphone + Intégration du numéro de téléphone + Vie privée + Hôte du proxy + Mot de passe du proxy + Port du proxy + Type du proxy + Nom d\'utilisateur du proxy + Partager mon statut de lecture et afficher le statut de lecture des autres + Statut de lecture + Ré-autoriser le compte + Effacer + Retirer le compte + Veuillez confirmer votre intention de retirer le compte en cours. + Verrouiller %1$s avec le verrouillage d’écran Android ou une méthode biométrique supportée + Délai d\'inactivité avant verrouillage de l\'écran + Écran de verrouillage + Empêche les captures d\'écran dans la liste récente et dans l\'application + Sécurité de l\'écran + La version du serveur est très ancienne et ne sera plus prise en charge dans la prochaine version  ! + La version du serveur est trop ancienne et n\'est pas supporté par cette version de l\'application android + Serveur non pris en charge + L\'application de notifications n\'est pas installée sur le serveur + Réglé par l\'économiseur de batterie + Sombre + Utiliser les paramètres du système + thème + Clair + Thème + Partager mon statut de saisie et montrer le statut de saisie des autres + Le statut de saisie n\'est disponible que lors de l\'utilisation d\'un backend haute performance (HPB). + État de saisie + Le proxy requiert les informations d\'identification + Attention + Seul le compte courant peut être ré-autorisé + Partager le contact + Permission de lire les contacts requise + Partager la position actuelle + Lien de partage + Partager la position + Partager cette position + Sélectionnez un compte + Éléments partagés + Carte de l\'application Deck + Images, fichiers, messages vocaux… + Pas d\'éléments partagés + Position + Position partagée + Quand les notifications ne sont pas correctement configurées, afficher un avertissement régulier + Afficher un avertissement régulier à propos des notifications + Trier par + Démarrer une discussion de groupe + Heure de début + Changer de compte + Équipe + Tester les notifications push + Résultats des tests + Aujourd\'hui à %1$s + Demain à %1$s + Choisir des fichiers + Envoyer ces fichiers à %1$s ? + Envoyer ce fichier à %1$s ? + Désolé, l\'envoi a échoué + Échec du téléversement de %1$s + Échec + Partagé depuis %1$s + Téléverser depuis l\'appareil + Téléversement + %1$s à %2$s - %3$s\%% + Prendre une photo + Enregistrer une vidéo + Utilisateur + Enregistrement vidéo de %1$s + Enregistrement de conversation de %1$s (%2$s) + Maintenir pour enregistrer, relâcher pour envoyer. + La permission d\'enregistrer l\'audio est requise + « Balayer pour annuler + Webinaire + Oui + Semaine suivante + Aucune conversation archivée + Aucun message hors-ligne trouvé + Pas d\'intégration avec le carnet d\'adresses à cause d\'autorisations manquantes + Seulement les mentions @ + Désactiver + Défaut + Utiliser les paramètres de la conversation + 1 heure + En ligne + Statut de connexion + Discussions en cours + Ouvrir dans l\'application Fichiers + Ouvrir les Notes + Aller à la conversation + Jouer / mettre en pause le message vocal + Contrôle de la vitesse de lecture + Ajouter une option + Modifier le vote + Fin du sondage + Voulez-vous réellement terminer ce sondage ? Cela ne peut pas être annulé. + Vous ne pouvez pas voter pour plus de choix pour ce sondage. + Réponses multiples + Supprimer l\'option %1$d + Option %1$d + Options + Sondage privé + Question + Votre question + Résultats + Paramètres + Vote + Vote soumis + Précédemment défini + Le code QR ne peut être lu + Lever la main + Tout + le partage de fichier n\'est pas possible sans les permissions + Conversations récentes + L\'appel est enregistré + Annuler le lancement de l\'enregistrement + L\'enregistrement a échoué. Veuillez contacter votre administrateur. + Lancer l\'enregistrement + Voulez-vous vraiment arrêter l\'enregistrement ? + Arrêter l\'enregistrement de l\'appel + Arrêter l\'enregistrement + Arrêt de l\'enregistrement… + Le consentement à l\'enregistrement est exigé pour tous les appels + L\'enregistrement peut inclure votre voix, la vidéo de votre caméra et le partage d\'écran. Votre consentement est requis avant de rejoindre l\'appel. Consentez-vous ? + Exiger le consentement à l\'enregistrement avant de rejoindre l\'appel dans cette conversation + Consentement à l\'enregistrement + L\'appel peut être enregistré. + Enregistrement + La conversation %1$s a été supprimée des favorites + La conversation %1$s a été renommée + Renvoyer + Réinitialiser le statut + Il est impossible de rejoindre une autre salle pendant un appel + Sauvegarder + Scanner le QR Code + Synchronisation avec les serveurs de confiance uniquement + Fédéré + Visible uniquement aux personnes dans l\'instance et aux invités + Local + Visible uniquement pour les personnes correspondantes via l\'intégration du numéro de téléphone dans Talk pour mobile + Privé + Synchronisation avec les serveurs de confiance et le carnet d\'adresses public global + Publié + Basculer la visibilité + Changer le niveau de confidentialité de %1$s + Défiler jusqu\'en bas + Icône de recherche + secondes avant + Sélectionné + Envoyer un e-mail + Envoyer a + Vous n\'êtes pas autorisé à partager du contenu dans cette conversation + Envoyer à… + Envoyer sans notification + Affecter + Définir l\'avatar depuis la caméra + Définir le statut + Définir le message de statut + Partage + Rejoindre la conversation %1$s sur %2$s + Son + Fichier + Média + Autre + Sondage + Enregistrement d\'appel + Voix + Montrer la raison du bannissement + Afficher les participants bannis + Favori + Vous n\'êtes pas autorisé à lancer un appel + Créer une conversation + a lancé un appel + Message d\'état + Statut rétabli + Basculer vers la salle de sous-groupe + Basculer vers la salle principale + Prendre une photo + Erreur en prenant la photo + Impossible de prendre une photo sans permissions + Re-prendre la photo + Envoyer + Retourner la caméra + Rogner la photo + Réduire la taille de l\'image + Allumer / Éteindre la lampe + 30 minutes + Cette semaine + Ceci est un message de test + Ce week-end + Annuler la création de la conversation + Notifications de conversation + Répondre + Titre de la conversation + Aujourd\'hui + Demain + Traduire + Traduction + Copier le texte traduit + Détecter la langue + Paramètres de l\'appareil + Impossible de détecter la langue + Échec de la traduction + De + À + et 1 autre sont en train de taper… + sont en train de taper... + est en train de taper... + et %1$s autres sont en train de taper… + Désarchiver la conversation + Une fois qu\'une conversation est désarchivée, elle sera à nouveau affichée par défaut. + %1$s non archivée + Retirer le ban + Non lu + Téléverser un nouvel avatar depuis l\'appareil + %1$s est absent et pourrait ne pas répondre. + %1$s est absent aujourd\'hui + Remplacement : + Avatar utilisateur + Adresse + Nom complet + E-mail + Numéro de téléphone + Twitter + Site web + État + Impossible de récupérer les informations personnelles de l\'utilisateur. + Aucunes informations personnelles renseignées + Ajoutez vos nom, photo et coordonnées sur votre page de profil. + Appel vidéo + Quel est votre statut ? + + Voir %d message similaire + Voir %d messages similaires + Voir %d messages similaires + + + Cette conversation sera automatiquement supprimée pour tout le monde après %1$d jour d\'inactivité + Cette conversation sera automatiquement supprimée pour tout le monde après %1$d jours d\'inactivité + Cette conversation sera automatiquement supprimée pour tout le monde après %1$d jours d\'inactivité + + + %d réponses + %d réponses + %d réponses + + + %d vote + %d votes + %d votes + + diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml new file mode 100644 index 0000000..ceaef09 --- /dev/null +++ b/app/src/main/res/values-ga/strings.xml @@ -0,0 +1,739 @@ + + + Cuir in eagar + Cuir + Cuir le Nótaí + Cuireadh comhrá %1$s le ceanáin + Cuardaigh i %s + Le feiceáil as líne + Comhrá cartlainne + Nuair a bheidh comhrá curtha i gcartlann, cuirfear i bhfolach é de réir réamhshocraithe. Roghnaigh an scagaire \"Cartlannaithe\" chun féachaint ar chomhráite cartlainne. Gheofar tagairtí díreacha fós. + Cartlannaithe + Cartlannaithe %1$s + Glao fuaime + Bluetooth + Aschur fuaime + Fón + Cainteoir + Cluasán sreangaithe + Socraíodh do stádas go huathoibríoch + Avatar + Amach + Cnaipe ar ais + Bac + Cosc ar rannpháirtí + Liosta toirmisc + Gnóthach + Féilire + Ardroghanna glaonna + Tá an glao ar siúl ar feadh uair an chloig. + Glaoigh gan fógra + Cead ceamara tugtha. Roghnaigh ceamara arís le do thoil. + Cealaigh Logáil Isteach + Roghnaigh avatar ó scamall + Glan teachtaireacht stádais + Glan an teachtaireacht stádais ina dhiaidh + Dún + Dún Deilbhín + Ceangal bunaithe + Níl aon cheangal leis an bhfreastalaí + Ceangal caillte - Tá na teachtaireachtaí seolta i scuaine + Taifeadadh glas chun an teachtaireacht gutha a thaifeadadh go leanúnach + Tá an comhrá cartlannaithe + Tá an comhrá léite amháin + Theip ar an gcomhrá Inléite amháin a shocrú + Comhráite + Cruthaigh comhrá + Cruthaigh saincheist + Saincheaptha + Crios contúirte + %1$s i %2$s + Scrios an avatar + Scrios taifeadadh gutha + Scriosadh comhrá %1$s + Ná cur as + Ná soiléir + Cuir in eagar + Ní féidir teachtaireachtaí atá níos sine ná 24 uair an chloig a chur in eagar + Cuir teachtaireacht in eagar + le déanaí + Criptithe + Cuir deireadh leis an nglao + Cuir deireadh leis an nglao do chách + Tharla fadhb agus do chomhráite á lódáil + Tharla earráid agus rannpháirtí á bhaint de + Theip ar shábháil %1$s + 15 nóiméad + fillteán + Á lódáil… + %1$s (%2$d) + Snáitheanna leanta + 4 uair an chloig + Theip ar chuirí ar feitheamh a fháil + (in eagar) + Nóta inmheánach + Dofheicthe + Níorbh fhéidir teangacha a aisghabháil + Theip ar an aisghabháil + Níos déanaí inniu + Fág glaoch + D\'fhág tú an comhrá %1$s + Íoslódáil níos mó torthaí + Am áitiúil: %1$s + Cead suímh diúltaithe + Cumasaigh é i socruithe an aip le do thoil + Seirbhísí suímh díchumasaithe + Cumasaigh seirbhísí suímh (GPS) le go mbeidh tú in ann an ghné seo a úsáid + Cuir glas ar an gcomhrá + Siombail ghlais + Lámh íochtair + Marcáladh comhrá %1$s mar léite + Marcáladh comhrá %1$s mar neamhléite + Luaite + Is nua ar dtús + Is sine ar dtús + A - Z + Z - A + An ceann is mó ar dtús + Is lú ar dtús + Teachtaireacht cóipeáilte + An bhfuil tú cinnte gur mian leat an teachtaireacht seo a scriosadh? + Scrios tú an teachtaireacht + Curtha in eagar ag %1$s + Tapáil chun vótaíocht a oscailt + Gan torthaí cuardaigh + Tosaigh ag clóscríobh chun cuardach a dhéanamh… + Cuardaigh… + Teachtaireachtaí + Balbhaigh gach fógra + Tá cuntas roghnaithe iompórtáilte anois agus ar fáil + Faoi + Úsáideoir gníomhach + Cuir cuntas leis + Tá an cuntas sceidealta le scriosadh, agus ní féidir é a athrú + Oscail an príomh-roghchlár + Cuir ceangaltán leis + Cuir emojis leis + Cuir leis an gcomhrá + Cuir rannpháirtithe leis + Cuir le ceanáin + Ceart go leor, déanta go léir! + Bioráin: %1$s + Díghlasáil %1$s + Chun cainteoirí Bluetooth a chumasú, deonaigh cead \"Gléasanna in aice láimhe\" le do thoil. + Freagair mar fhísghlao + Freagair mar ghlao gutha amháin + Athraigh aschur fuaime + Scoránaigh ceamara + Croch suas + Scoránaigh micreafón + Oscail mód pictiúr-i-pictiúr + Athraigh go físeán féin + ISTEACH + Ainm an chomhrá + Fógraí glaonna + %1$s d\'ardaigh an lámh + Ag athcheangal… + AG FUAIMNIÚ + %1$sar glaoch + %1$s le fón + %1$s le físeán + Gan freagra i gceann 45 soicind, tapáil chun triail eile a bhaint as + %s glaoch + %sfísghlao + %s glao gutha + Chun físchumarsáid a chumasú deonaigh cead \"Ceamara\" le do thoil. + Cealaigh + Theip ar na cumais a fháil, ag cur deireadh leis + Fotheideal + An bhfuil muinín agat as an teastas SSL anaithnid go dtí seo, arna eisiúint ag %1$s le haghaidh %2$s, bailí ó %3$s go %4$s? + Seiceáil an teastas + Chuir do shocrú SSL cosc ​​ar cheangal + Athraigh teastas fíordheimhnithe + Athraigh do phasfhocal + Cealaigh eagarthóireacht + Cealaigh eagarthóireacht + Scrios gach teachtaireacht + Scriosadh gach teachtaireacht + An bhfuil tú cinnte gur mhaith leat gach teachtaireacht sa chomhrá seo a scriosadh? + Athraigh teastas an chliaint + Socraigh teastas cliant + agus + Cóipeáil + Cóipeáladh chuig an ngearrthaisce + Cruthaigh + Faoi mhíchumas + Díbhe + Tá brón orm, chuaigh rud éigin mícheart! + Tuilleadh roghanna + Socraigh + Scipeáil + Anaithnid + Roghnaigh teastas fíordheimhnithe + Ag nascadh… + Déanta + Cur síos ar an gcomhrá + Eolas faoin gcomhrá + Glao físe + Glao gutha + Comhrá gan aimsiú + Socruithe comhrá + Glac páirt i gcomhrá nó cuir tús le ceann nua + Abair Dia duit a chairde agus a chomhghleacaithe! + Cóipeáil + Cruthaigh comhrá nua + Cruthaigh vótaíocht + Tusa: + Inniu + Inné + Scrios + Scrios go léir + Scrios an comhrá + Má scriosann tú an comhrá, scriosfar é do gach rannpháirtí eile freisin. + Scrios teachtaireacht + D\'éirigh leis an teachtaireacht a scriosadh, ach seans gur sceitheadh ​​chuig seirbhísí eile í + Scrios anois + Baineadh úsáideoir %1$s + Léim as an modhnóir + Taifead teachtaireacht gutha + Seol teachtaireacht + Cuntas reatha + Freastalaí + Suiteáilte aip fógra freastalaí? + Úsáideoir + Stádas úsáideora cumasaithe? + leagan Android + Aip + Ainm aip + Úsáideoirí cláraithe + Leagan aip + Déantar neamhaird de bharrfheabhsú ceallraí, go breá + Tá optamú ceallraí cumasaithe rud a d\'fhéadfadh fadhbanna a chruthú. Ba cheart duit leas iomlán a bhaint as ceallraí a dhíchumasú! + Socruithe ceallraí + Gléas + Oscail seicliosta fabhtcheartaithe + Oscail scáileán diagnóis + Oscail dontkillmyapp.com + Íoslódáil an comhartha brú firebase is déanaí + Giniúint comhartha brú firebase is déanaí + Níl aon tacar comhartha brú firebase. Cruthaigh tuairisc ar fhabht le do thoil. + Comhartha brú firebase + Níl seirbhísí Google Play ar fáil. Ní thacaítear le fógraí + Seirbhísí Google Play + Tá seirbhísí Google Play ar fáil + Clárú brú is déanaí ag seachfhreastalaí bhrú + Gan chlárú fós ag seachfhreastalaí bhrú + Clárú brúigh is déanaí ar an bhfreastalaí + Níl sé cláraithe ag an bhfreastalaí fós + Meta faisnéise + Giniúint tuairisc chórais + Cumasaíodh cainéal fógra glaonna? + Cumasaíodh cainéal fógraí teachtaireachtaí? + Ceadanna fógraí + Fón + Leagan Freastalaí Talk + Leagan freastalaí + Seachtrach + Inmheánach + Mód Comharthaíochta + Pasfhocal neamhbhailí + Tá an freastalaí i mód cothabhála faoi láthair. + Tá an feidhmchlár as dáta + Tá an aip ró-shean agus ní thacaíonn an freastalaí seo léi a thuilleadh. Nuashonraigh le do thoil. + Nuashonrú + Ar mhaith leat an cuntas seo a athúdarú nó a scriosadh? + Má shábhálann tú an meán seo go stóráil, ligfidh sé d\'aon aipeanna eile ar do ghléas rochtain a fháil air. + Leanúint ar aghaidh? + Níl + Sábháil go stóras? + + Níorbh fhéidir an t-ainm taispeána a fháil, ag éirí as + Níorbh fhéidir an t-ainm taispeána a stóráil, ag cur deireadh leis + Cuir in eagar + Cuir in eagar + Cuir teachtaireacht in eagar + Arna chur in eagar ag riarthóir + Roghchlár comhrá imeachta + Sceideal + 8 n-uaire + 4 seachtaine + as + 1 lá + 1 uair + 1 seachtain + Dul in éag teachtaireachtaí comhrá + Is féidir teachtaireachtaí comhrá a bheith imithe in éag tar éis am áirithe. Nóta: Ní scriosfar comhaid a roinntear sa chomhrá don úinéir, ach ní roinnfear iad sa chomhrá a thuilleadh. + Theip ar na socruithe comharthaíochta a fháil + Glac + Diúltaigh + ó %1$s ag %2$s + Níl aon chuirí ar feitheamh + Tá cuirí ar feitheamh agat + Ar ais + Teastaíonn cead chun comhaid a rochtain + Scag comhráite + Úsáideoir ag leanúint nasc poiblí + Tusa: %1$s + Ar aghaidh + Ar aghaidh chuig… + Gailearaí + Nach bhfuil freastalaí agat fós?\nCliceáil anseo chun ceann a fháil ó sholáthraí + Faigh cód foinse + Grúpa + Aoi + Rochtain aoi + Ní féidir rochtain aoi a chumasú/a dhíchumasú. + Lig d\'aíonna nasc poiblí a roinnt chun páirt a ghlacadh sa chomhrá seo. + Ceadaigh aíonna + Cuir isteach pasfhocal + Pasfhocal rochtana aoi + Earráid le linn an pasfhocal a shocrú/a dhíchumasú. + Socraigh pasfhocal chun srian a chur ar na daoine ar féidir leo an nasc poiblí a úsáid. + Cosaint pasfhocal + Cuir cuirí ar ais + Níor seoladh cuirí mar gheall ar earráid. + Cuireadh cuirí amach arís. + Roinn nasc an chomhrá + Cuir isteach teachtaireacht… + Ní thugtar aird ar bharrfheabhsú ceallraí. Ba cheart é seo a athrú chun a chinntiú go n-oibríonn fógraí sa chúlra! Cliceáil OK agus roghnaigh \"Gach aip\" -> %1$s -> Ná optamaigh le do thoil + Déan neamhaird de bharrfheabhsú ceallraí + Comhrá tábhachtach + Déantar neamhaird ar stádas an úsáideora \"Ná cuir isteach\" i gcás comhráite tábhachtacha + Am neamhbhailí + cuirí + Glac páirt i gcomhráite oscailte + Coinnigh + Ní mór duit modhnóir nua a chur chun cinn sula bhféadfaidh tú an comhrá a fhágáil + %1$s | Athraithe is déanaí: %2$s + Fág an comhrá + Ag fágáil an ghlao… + Ceadúnas Ginearálta Poiblí GNU, Leagan 3 + Ceadúnas + Tá teorainn %s carachtar buailte + Stocaireacht + Tá an cruinniú seo sceidealaithe le haghaidh %1$s + Cuirfear tús leis an gcruinniú go luath + Tá tú ag fanacht sa stocaireacht faoi láthair. + Do shuíomh reatha + Tá cead suímh ag teastáil + Post anaithnid + Faoi ghlas + Tapáil chun díghlasáil + Gan socraithe + Marcáil mar léite + Marcáil mar neamhléite + Comhrá marcáilte mar thábhachtach + Dímharcáladh an comhrá mar chomhrá íogair + Comhrá marcáilte mar íogair + Dímharcáladh an comhrá mar chomhrá tábhachtach + Deireadh leis an gcruinniú + Teachtaireacht curtha leis na nótaí + Theip + Theip ar an teachtaireacht a sheoladh: + As líne + Cealaigh freagra + Léadh an teachtaireacht + Ag seoladh + Teachtaireacht seolta + Tá an micreafón cumasaithe agus tá fuaim á taifeadadh + Tabhair cead \"Micreafón\" le do thoil chun cumarsáid gutha a chumasú. + Chaill tú glao ó %s + Modhnóir + Comhrá nua + Infheictheacht + Luaite gan léamh + Teachtaireachtaí gan léamh + Níl %1$s ar fáil (gan suiteáil nó srianta ag an riarthóir) + Aoi + Níl + Níl aon chomhrá oscailte + Níl aon chomhrá oscailte ar féidir leat páirt a ghlacadh ann.\nNíl aon chomhráite oscailte ann nó chuir tú isteach ar gach ceann acu cheana féin. + Gan seachfhreastalaí + Níl cead agat an fhuaim a ghníomhachtú! + Níl cead agat físeáin a ghníomhachtú! + Ní anois + %1$s ar chainéal fógra %2$s + Glaonna + Fógra a thabhairt faoi ghlaonna isteach + Teachtaireachtaí + Fógra faoi theachtaireachtaí ag teacht isteach + uaslódálacha + Fógra faoi dhul chun cinn an uaslódála + Socruithe fógra + Níl fógraí socraithe i gceart + Tá cead fógartha agus socruithe ceallraí socraithe i gceart chun fógraí a fháil. Má tá fadhbanna agat fógraí a fháil ar aon nós, seiceáil le do thoil an bhfuil na bealaí fógra le haghaidh glaonna agus teachtaireachtaí cumasaithe. Is féidir tuilleadh cabhrach a fháil ag DontKillMyApp.com nó ag an seicliosta fabhtcheartaithe. Mura gcabhraíonn sé seo, téigh chuig an scáileán diagnóise agus seol tuairisc ar fhabht le do thoil. + Fabhtcheartú fógraí + Cuir in iúl i gcónaí + Fógra a thabhairt nuair a luaitear + Ná cuir in iúl riamh + Faoi láthair, seiceáil do nascacht le do thoil + Ceart go leor + Cruinniú leanúnach + Oscail an comhrá d\'úsáideoirí cláraithe + Chomh maith leis sin oscailte d\'úsáideoirí aip aoi + Úinéir + Rannpháirtithe + Cuir rannpháirtithe leis + Pasfhocal + Socraigh ceadanna + Diúltaíodh roinnt ceadanna. + Ceadaigh ceadanna le do thoil + Oscail socruithe + Tabhair ceadanna ag Socruithe > Ceadanna le do thoil + Cuntas gan aimsiú + Déan comhrá trí %s + Balbhaigh micreafón + Cumasaigh micreafón + Teachtaireachtaí + Príobháideacht + Eolas Pearsanta + Cur chun cinn chuig modhnóir + Comhrá poiblí + Díchumasaíodh fógraí brúigh + Tá brón orm gur tharla fadhb, is é %1$s an earráid + Tá brón orm gur tharla rud éigin mícheart, ní féidir teachtaireacht tástála a fháil + Tá fógra brú seolta go rathúil. Ba chóir duit fógra a fháil anois ar an ngléas seo leis an teideal \'Fógraí brú á dtástáil\'. + Brú-chun-caint + Agus an micreafón díchumasaithe, cliceáil agus coinneáil chun Brúigh chun Caint a úsáid + Agus an micreafón díchumasaithe, cliceáil agus coinneáil chun Brúigh chun Caint a úsáid + Bain ó cheanáin + Bain grúpa agus baill + Bain rannpháirtí + Bain Pasfhocal + Bain foireann agus baill + Fuaim iargúlta múchta + Athainmnigh + Freagra + Freagair go príobháideach + Coinníodh an seomra go rathúil + Sábháil + Sábháilte go rathúil + 30 soicind + 5 nóiméad + 1 nóiméad + 10 nóiméad + Láithreach + 600 + 60 + 30 + 300 + Cuardach + Glan cuardach + Roghnaigh cuntas + Nuashonraigh teachtaireacht + Seol taifeadadh gutha + Comhrá íogair + Díchumasófar réamhamharc teachtaireachta sa liosta comhráite agus sna fógraí + Sheol %1$s GIF. + Sheol tú GIF. + Sheol %1$s físeán. + Sheol tú físeán. + Sheol %1$s fuaim. + Sheol tú fuaim. + Sheol %1$s íomhá. + Sheol tú íomhá. + Sheol %1$s cárta deic + Nasc freastalaí tástála + Uasghrádaigh do bhunachar sonraí %1$s le do thoil + Theip ar iompórtáil an chuntais roghnaithe + An nasc chuig do chomhéadan gréasáin %1$s nuair a osclaíonn tú é sa bhrabhsálaí. + Iompórtáil cuntas ón aip %1$s + Iompórtáil cuntas + Iompórtáil cuntais ón aip %1$s + Iompórtáil cuntais + Tabhair do %1$s as cothabháil le do thoil + Críochnaigh do %1$s shuiteáil le do thoil + Tástáil nasc + Níl an aip Talk tacaithe ag an bhfreastalaí suiteáilte + Seoladh an fhreastalaí https://… + Ní oibríonn %1$s ach le %2$s 13 agus níos sine + Socraigh pasfhocal nua + Socraigh Pasfhocal + Socruithe + Nuashonraíodh do chuntas a bhí ann cheana féin, in ionad ceann nua a chur leis + Casta + Dealramh + Glaonna + Déan teagmháil le riarthóir na + Oscail scáileán diagnóis chun socruithe a sheiceáil nó chun tuairisc fhabht a chruthú + Diagnóis + Tugann sé treoir don mhéarchlár foghlaim phearsantaithe a dhíchumasú (gan ráthaíochtaí) + Méarchlár incognito + Gan fuaim + Níl aip Talk suiteáilte ar an bhfreastalaí a ndearna tú iarracht fíordheimhniú ina aghaidh + Fógraí + Diúltaítear d’fhógraí + Deonaítear fógraí + Teachtaireachtaí + Meaitseáil teagmhálaithe bunaithe ar uimhir theileafóin chun aicearra Talk a chomhtháthú san aip teagmhálacha córais + Earráid 429 An Iomarca Iarratas + Is féidir leat d’uimhir theileafóin a shocrú ionas go mbeidh úsáideoirí eile in ann teacht ort + Cuir isteach uimhir theileafóin + Uimhir theileafóin neamhbhailí + D\'éirigh leis an uimhir theileafóin a shocrú + Uimhir teileafón + Comhtháthú uimhir theileafóin + Príobháideacht + Óstach seachfhreastalaí + Pasfhocal seachfhreastalaí + Port seachfhreastalaí + Cineál seachfhreastalaí + Ainm úsáideora seachfhreastalaí + Roinn mo stádas léite agus taispeáin stádas léite daoine eile + Léigh stádas + Athúdaraigh cuntas + Bain + Bain cuntas + Deimhnigh do rún an cuntas reatha a bhaint le do thoil. + Glasáil %1$s le glas scáileáin Android nó modh bithmhéadrach a dtacaítear leis + Teorainn ama neamhghníomhaíochta an ghlais scáileáin + Glasáil scáileáin + Cosc ar screenshots sa liosta le déanaí agus taobh istigh den aip + Slándáil scáileáin + Tá leagan an fhreastalaí an-sean agus ní thacófar leis sa chéad eisiúint eile! + Tá leagan an fhreastalaí ró-shean agus ní thacaíonn an leagan seo den aip Android leis + Freastalaí nach dtacaítear leis + Níl aip fógraí freastalaí suiteáilte + Socraithe ag spárálaíscáileáin ceallraí + Dorcha + Úsáid réamhshocraithe an chórais + téama + Solas + Téama + Roinn mo stádas clóscríofa agus taispeáin stádas clóscríofa daoine eile + Níl stádas clóscríofa ar fáil ach amháin nuair a úsáidtear inneall ardfheidhmíochta (HPB) + Stádas clóscríofa + Teastaíonn dintiúir ó sheachvótálaí + Rabhadh + Ní féidir ach cuntas reatha a athúdarú + Roinn teagmhála + Teastaíonn cead chun teagmhálaithe a léamh + Roinn an suíomh reatha + Comhroinn nasc + Comhroinn suíomh + Roinn an suíomh seo + Roghnaigh cuntas + Míreanna roinnte + Cárta deic + Íomhánna, comhaid, teachtaireachtaí gutha… + Níl aon mhír roinnte + Suíomh + Suíomh roinnte + Nuair nach bhfuil fógraí socraithe i gceart, taispeáin rabhadh rialta + Taispeáin rabhadh fógra rialta + Sórtáil de réir + Tosaigh comhrá grúpa + Am tosaithe + Athraigh cuntas + Foireann + Tástáil fógraí brú + Torthaí tástála + Inniu ag %1$s + Amárach ag %1$s + Roghnaigh comhaid + An bhfuil fonn ort na comhaid seo a sheoladh chuig %1$s? + An bhfuil fonn ort an comhad seo a sheoladh chuig %1$s? + Ár leithscéal, theip ar an uaslódáil + Theip ar uaslódáil %1$s + Teip + Roinn ó %1$s + Íosluchtaigh ó gléas + Ag uaslódáil + %1$s chun %2$s - %3$s\%% + Tóg pictiúr + Tóg físeán + Úsáideoir + Taifeadadh físe ó %1$s + Taifeadadh cainte ó %1$s (%2$s) + Coinnigh a thaifeadadh, scaoileadh a sheoladh. + Tá cead taifeadta fuaime ag teastáil + « Sleamhnán le cur ar ceal + webinar + + An tseachtain seo chugainn + Níl aon chomhráite sa chartlann + Níor sábháladh aon teachtaireacht as líne + Níl aon chomhtháthú uimhir theileafóin mar gheall ar cheadanna in easnamh + Gach teachtaireacht + \@-luaite amháin + as + Réamhshocrú + Lean socruithe comhrá + 1 uair + Ar líne + Stádas ar líne + Oscail comhráite + Oscail san aip Comhaid + Nótaí Oscailte + Téigh go dtí an snáithe + Seinn/cuir teachtaireacht gutha ar sos + Rialú luas athsheinm + Cuir rogha leis + Cuir vóta in eagar + Deireadh vótaíocht + Ar mhaith leat deireadh a chur leis an bpobalbhreith seo? Ní féidir é seo a chealú. + Ní féidir leat vótáil le tuilleadh roghanna don vótaíocht seo. + Freagraí iolracha + Scrios rogha %1$d + Rogha %1$d + Roghanna + Vótaíocht phríobháideach + Ceist + Do cheist + Torthaí + Socruithe + Vóta + Vóta curtha isteach + Socraíodh roimhe seo + Níorbh fhéidir an cód QR a léamh + Ardaigh lámh + Gach + Ní féidir comhaid a roinnt ón stóras gan ceadanna + Snáitheanna le déanaí + Tá an glao á thaifeadadh + Cealaigh tús an taifeadta + Theip ar an taifeadadh. Déan teagmháil le do riarthóir. + Tosaigh taifeadadh + Ar mhaith leat i ndáiríre an taifeadadh a stopadh? + Stop Taifeadadh Glaonna + Stop taifeadadh + Ag stopadh an taifeadadh… + Tá toiliú taifeadta ag teastáil le haghaidh gach glao + Seans go gcuimseodh an taifeadadh do ghuth, físeáin ó cheamara, agus comhroinnt scáileáin. Tá do thoiliú ag teastáil sula nglacann tú páirt sa ghlao. An thoilíonn tú? + Teastaíonn toiliú taifeadta sula nglacann tú páirt sa ghlao seo + Toiliú taifeadta + Seans go ndéanfar an glao a thaifeadadh. + Taifeadadh + Baineadh comhrá %1$s de cheanáin + Athainmníodh comhrá %1$s + Seol ar ais + Stádas a athshocrú + Ní féidir dul isteach i seomraí eile agus tú ar ghlao + Sábháil + Scan QR Code + Sioncrónaigh le freastalaithe iontaofa amháin + Cónaidhme + Infheicthe ag daoine sa chás seo agus ag aíonna amháin + Áitiúil + Le feiceáil ag daoine amháin a mheaitseáiltear trí chomhtháthú uimhir theileafóin trí Talk on mobile + Príobháideach + Sioncrónaigh le freastalaithe iontaofa agus leis an leabhar seoltaí domhanda agus poiblí + Foilsithe + Scoránaigh scóip + Athraigh leibhéal príobháideachta %1$s + Scrollaigh go bun + Deilbhín Cuardaigh + soicind ó shin + Roghnaithe + Seol ríomhphost + Sheoladh chuig + Níl cead agat ábhar a roinnt leis an gcomhrá seo + Sheoladh chuig … + Seol gan fógra + Socraigh + Íosluchtaigh avatar ó ceamara + Socraigh stádas + Socraigh teachtaireacht stádais + Comhroinn + Glac páirt i gcomhrá %1$s ag %2$s + Fuaime + Comhad + Meáin + Eile + Vótaíocht + Taifeadadh glaonna + Guth + Taispeáin Cúis Toirmeasc + Taispeáin rannpháirtithe toirmiscthe + is fearr leat + Níl cead agat glao a thosú + Cruthaigh snáithe + thosaigh glao + Teachtaireacht stádais + Stádas ar ais + Athraigh go seomra ar leithligh + Téigh go dtí an príomhsheomra + Glac grianghraf + Earráid agus pictiúr á thógáil + Ní féidir grianghraf a ghlacadh gan ceadanna + Glac grianghraf arís + Seol + Athraigh ceamara + Grianghraf barr + Laghdaigh méid na híomhá + Scoránaigh tóirse + 30 nóiméad + An tseachtain seo + Is teachtaireacht tástála é seo + An deireadh seachtaine seo + Cealaigh cruthú snáithe + Fógraí snáithe + Freagra + Teideal an snáithe + Níor aimsíodh aon snáitheanna + Inniu + Amárach + Aistrigh + Aistriúchán + Cóipeáil téacs aistrithe + Braith teanga + Socruithe gléis + Níorbh fhéidir teanga a bhrath + Theip ar an aistriúchán + Ó + Chun + agus tá 1 eile ag clóscríobh … + ag clóscríobh… + ag clóscríobh… + agus %1$s eile ag clóscríobh … + Comhrá gan chartlann + Nuair a bheidh comhrá gan chartlannú, taispeánfar arís é de réir réamhshocraithe. + Gan chartlann %1$s + Dícosc + Neamhléite + Íosluchtaigh avatar nua ar an teileafón + Tá %1$s as oifig agus seans nach bhfreagróidh sé + Tá %1$s as oifig inniu + Athsholáthar: + Avatar úsáideoir + Seoladh + Ainm iomlán + Ríomhphost + Uimhir teileafón + Twitter + Suíomh Gréasáin + Stádas + Theip ar aisghabháil faisnéise pearsanta úsáideora. + Níl aon tacar faisnéise pearsanta ann + Cuir ainm, pictiúr agus sonraí teagmhála ar do leathanach próifíle. + Glao físe + Cad é do stádas? + + Féach ar %d teachtaireacht dá samhail + Féach ar %d teachtaireacht dá samhail + Féach ar %d teachtaireacht dá samhail + Féach ar %d teachtaireacht dá samhail + Féach ar %d teachtaireacht dá samhail + + + Scriosfar an comhrá seo go huathoibríoch do gach duine i %1$d lá gan aon ghníomhaíocht + Scriosfar an comhrá seo go huathoibríoch do gach duine i %1$d lá gan aon ghníomhaíocht + Scriosfar an comhrá seo go huathoibríoch do gach duine i %1$d lá gan aon ghníomhaíocht + Scriosfar an comhrá seo go huathoibríoch do gach duine i %1$d lá gan aon ghníomhaíocht + Scriosfar an comhrá seo go huathoibríoch do gach duine i %1$d lá gan aon ghníomhaíocht + + + %d freagra + %d freagraí + %d freagraí + %d freagraí + %d freagraí + + + %d vóta + %d vótaí + %d vótaí + %d vótaí + %d vótaí + + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000..d5a807a --- /dev/null +++ b/app/src/main/res/values-gl/strings.xml @@ -0,0 +1,727 @@ + + + Editar + Engadir + Engadir a Notas + Engadiuse a conversa %1$s aos favoritos + Buscar en %s + Aparecer como sen conexión + Arquivar a conversa + Unha vez arquivada unha conversa, de xeito predeterminado vai ser agochada. Seleccione o filtro «Arquivado» para ver as conversas arquivadas. Seguirá a recibir as mencións directas. + Arquivada + %1$s foi arquivada + Chamada de voz + Bluetooth + Saída de son + Teléfono + Altofalante + Auriculares con cable + O seu estado foi estabelecido automaticamente + Avatar + Ausente + Botón «Atrás» + Expulsión + Expulsar o participante + Lista de expulsións + Ocupado + Calendario + Opcións avanzadas de chamada + A chamada leva unha hora en curso. + Chamar sen notificación + Concedeuse o permiso para a cámara. Escolla a cámara de novo. + Cancelar o acceso + Escoller un avatar da nube + Limpar a mensaxe de estado + Limpar a mensaxe de estado após + Pechar + Icona «Pechar» + Estabeleceuse a conexión + Non hai conexión co servidor + Perdeuse a conexión — As mensaxes enviadas póñense en cola + Bloquear a gravación para gravar continuamente a mensaxe de voz + A conversa está arquivada + A conversa é só de lectura + Produciuse un fallo ao definir a conversa como de só lectura + Conversas + Crear conversa + Crear unha incidencia + Personalizado + Zona de perigo + %1$s en %2$s + Eliminar avatar + Eliminar a gravación de voz + A conversa %1$s foi eliminada + Non molestar + Non limpar + Editar + Non é posíbel editar as mensaxes con máis de 24 horas + Editar a mensaxe + Recente + Cifrado + Finalizar a chamada + Finalizar a chamada para todos + Produciuse un problema ao cargar as súas parolas + Produciuse un erro ao retirar a expulsión dun participante + Produciuse un fallo ao gardar %1$s + 15 minutos + cartafol + Cargando… + %1$s (%2$d) + Fíos seguidos + 4 horas + Produciuse un fallo ao recuperar os convites pendentes + (editado) + Nota interna + Invisíbel + Non foi posíbel recuperar os idiomas + Produciuse un fallo na recuperación + Hoxe máis tarde + Abandonar a chamada + Vde. deixou a conversa %1$s + Cargando máis resultados + Hora local: %1$s + Permiso de localización denegado + Actíveo nos axustes da aplicación + Servizos de localización desactivados + Active os servizos de localización (GPS) para usar esta función + Bloquear a conversa + Símbolo de bloqueo + Baixar a man + A conversa %1$s foi marcada como lida + A conversa %1$s foi marcada como sen ler + Mencionado + Primeiro o máis recente + Primeiro o máis antigo + A - Z + Z - A + Primeiro o máis grande + Primeiro o máis pequeno + Copiouse a mensaxe + Confirma que quere eliminar esta mensaxe? + Mensaxe eliminada por Vde. + Editado por %1$s + Toque para abrir a enquisa + Sen resultados de busca + Comece a escribir para buscar… + Buscar… + Mensaxes + Enmudecer todas as notificacións + A conta seleccionada foi importada e xa está dispoñíbel + Sobre + Usuario activo + Engadir unha conta + A conta está programada para a súa eliminación e non pode ser cambiada + Abre o menú de accións + Engadir un anexo + Engadir «emojis» + Escribir á conversa + Engadir participantes + Engadir a favoritos + Conforme, todo feito! + Pin: %1$s + Desbloquear %1$s + Para activar os altofalantes Bluetooth, conceda os permiso para «Dispositivos próximos». + Responda como videochamada + Responda só como chamada de voz + Cambiar a saída do son + Cambiar a cámara + Colgar + Cambiar o micrófono + Abrir o modo imaxe en imaxe (picture in picture) + Cambiar ao vídeo propio + ENTRANTE + Nome da conversa + Notificacións de chamadas + %1$s ergueu a man + Volvendo conectar… + CHAMANDO + %1$s en chamada + %1$s con teléfono + %1$s con vídeo + Se non hai resposta en 45 segundos, toque para tentalo de novo + %s chamada + %s videochamada + %s chamada de voz + Para activar a comunicación por vídeo, conceda o permiso de «Cámara». + Cancelar + Produciuse un fallo ao recuperar as funcionalidades. Interrompendo. + Lenda + Quere confiar nun certificado SSL descoñecido, emitido por %1$s para %2$s, válido desde %3$s ata %4$s? + Verificar o certificado + A súa configuración SSL impediu a conexión + Cambiar o certificado de autenticación + Cambiar o contrasinal + Cancelar a edición + Cancelar a edición + Eliminar todas as mensaxes + Elimináronse todas as mensaxes + Confirma que quere eliminar todas as mensaxes desta conversa? + Cambiar o certificado do cliente + Configurar o certificado do cliente + e + Copiar + Copiado no portapapeis + Crear + Desactivado + Rexeitar + Desculpe, algo foi mal! + Máis opcións + Definir + Omitir + Descoñecido + Seleccionar o certificado de autenticación + Conectando… + Feito + Descrición da conversa + Información da conversa + Chamada de vídeo + Chamada de voz + Non se atopou a conversa + Axustes da conversa + Únase a unha conversa ou inicie unha nova + Saúde aos seus amigos e compañeiros! + Copiar + Crear unha nova conversa + Crear enquisa + Vde.: + Hoxe + Onte + Eliminar + Eliminar todo + Eliminar a conversa + Se elimina a conversa, tamén se eliminará para todos os demais participantes. + Eliminar a mensaxe + A mensaxe foi eliminada correctamente, pero é posíbel que se filtrase a outros servizos + Eliminar agora + O usuario %1$s foi retirado + Relegar de moderador + Gravar mensaxe de voz + Enviar a mensaxe + Conta actual + Servidor + A aplicación de notificacións do servidor está instalada? + Usuario + O estado de usuario está activado? + Versión Android + Aplicación + Nome da aplicación + Usuarios rexistrados + Versión da aplicación + Ignorase a optimización da batería, todo ben + A optimización da batería está activada, o que pode causar problemas. Debería desactivar a optimización da batería! + Axustes da batería + Dispositivo + Abrir a lista de comprobación de solución de problemas + Abrir a pantalla de diagnóstico + Abrir dontkillmyapp.com + Última recuperación do testemuño de envío de Firebase + Última xeración do testemuño de envío de Firebase + Non se definiu ningún testemuño de envío de Firebase. Cree un informe de fallos. + Testemuño de envío de Firebase + Non están dispoñíbeis os servizos de Google Play. Non se admiten notificacións + Servizos de Google Play + Os servizos de Google Play están dispoñíbeis + Último rexistro de envío no proxy de envío + Aínda non está rexistrado no proxy de envío + Último rexistro de envío no servidor + Aínda non está rexistrado no servidor + Metainformación + Xeración do informe do sistema + Activouse a canle de notificación de chamadas? + Activouse a canle de notificación de mensaxes? + Permisos de notificación + Teléfono + Versión de Parladoiro do servidor + Versión do servidor + Externo + Interna + Modo de sinalización + Contrasinal incorrecto + O servidor neste momento está en modo de mantemento. + A aplicación está desactualizada + A aplicación é demasiado antiga e xa non é compatíbel con este servidor. Actualícea. + Actualizar + Quere volver autorizar ou eliminar esta conta? + Se garda este multimedia no almacenamento, calquera outra aplicación do seu dispositivo poderá acceder a el. + Continuar? + Non + Quere gardalo no almacenamento? + Si + Non foi posíbel recuperar o nome para amosar, interrompendo + Non foi posíbel almacenar o nome para amosar, interrompendo + Editar + Editar + Editar a mensaxe + Editado por alguén de administración + Menú do evento de conversa + Programación + 8 horas + 4 semanas + Apagado + 1 día + 1 hora + 1 semana + Caducidade das mensaxes das parolas + As mensaxes das parolas poden caducar após dun tempo determinado. Nota: Os ficheiros compartidos na parola non se eliminarán para o propietario, mais xa non se compartirán na conversa. + Produciuse un fallo ao recuperar os axustes da sinalización + Aceptar + Rexeitar + desde %1$s ata %2$s + Non hai convites pendentes + Vde. ten convites pendentes + Atrás + Precísase de permiso para acceder ao ficheiro + Filtrar conversas + Usuario seguindo unha ligazón pública + Vde.: %1$s + Reenviar + Reenviar a… + Galería + Aínda non ten un servidor? \nPrema aquí para obter un dun provedor + Obter o código fonte + Grupo + Convidado + Acceso de convidado + Non é posíbel activar/desactivar o acceso para convidados. + Permite aos convidados compartir unha ligazón pública para unirse a esta conversa. + Permitir convidados + Introduza un contrasinal + Contrasinal de acceso para convidados + Produciuse un erro ao configurar/desactivar o contrasinal. + Definir un contrasinal para restrinxir quen pode usar a ligazón pública. + Protección por contrasinal + Volver enviar os convites + Non se enviaron os convites por mor dun erro. + Os convites foron enviados de novo. + Compartir a ligazón da conversa + Escriba unha mensaxe… + Non se ignora a optimización da batería. Isto debería cambiarse para asegurarse de que as notificacións funcionan en segundo plano. Prema en Aceptar e seleccione «Todas as aplicacións» → %1$s → Non optimizar + Ignorar a optimización da batería + Conversa importante + Nas conversas importantes, ignorarase o estado «Non molestar» + Hora incorrecta + Convites + Unirse a conversas abertas + Conservar + Debe promover un novo moderador antes de poder abandonar a conversa + %1$s | Última modificación: %2$s + Abandonar a conversa + Abandonando a chamada… + Licenza Pública Xeral GNU, versión 3 + licenza + Acadouse o límite de %s caracteres + Ástrago + Esta xuntanza está programada para %1$s + A xuntanza comezará en breve + Agora está agardando no ástrago. + A súa localización actual + é necesario o permiso de localización + Posición descoñecida + Bloqueado + Toque para desbloquear + Sen definir + Marcar como lido + Marcar como sen ler + Conversa marcada como importante + Conversa sen marcar como sensíbel + Conversa marcada como sensíbel + Conversa sen marcar como importante + Rematou a xuntanza + Mensaxe engadida ás notas + Fallado + Produciuse un fallo ao enviar a mensaxe: + Sen conexión + Cancelar a resposta + Mensaxe lida + Enviando + Mensaxe enviada + O micrófono está activado e estase a gravar o son + Para activar a comunicación por voz, conceda o permiso de «Micrófono». + Perdeu unha chamada de %s + Moderador + Nova conversa + Visibilidade + Mencións sen ler + Mensaxes sen ler + %1$s non está dispoñíbel (nin instalado nin restrinxido pola administración da instancia.) + Convidado + Non + Non hai conversas abertas + Non hai conversas abertas ás que poida unirse.\nNon hai conversas abertas ou xa se uníu a todas. + Sen proxy + Vde. non ten permiso para activar o son! + Vde. non ten permiso para activar o vídeo! + Agora non + %1$s na canle de notificación %2$s + Chamadas + Notificar sobre as chamadas entrantes + Mensaxes + Notificar sobre as mensaxes entrantes + Envíos + Notificar sobre o progreso do envío + Axustes de notificación + As notificacións non están configuradas correctamente + O permiso de notificación e os axustes da batería están configurados correctamente para recibir notificacións. Se ten problemas para recibir notificacións de todos os xeitos, comprobe se as canles de notificación para chamadas e mensaxes están activadas. Pódese atopar máis axuda en DontKillMyApp.com ou na lista de comprobación de solución de problemas. Se isto non lle é de axuda, vaia á pantalla de diagnóstico e envíe un informe de fallo. + Resolución de problemas de notificación + Notificar sempre + Notificar cando son mencionado + Non notificar nunca + Non ten conexión, verifique a súa conectividade + Aceptar + Xuntanza en curso + Abrir a conversa a usuarios rexistrados + Aberta tamén aos usuarios da aplicación convidados + Propietario + Participantes + Engadir participantes + Contrasinal + Establecer os permisos + Algúns permisos foron denegados. + Autorice os permisos + Abrir os axustes + Conceda os permisos en Axustes > Permisos + Non se atopou a conta + Parolar a través de %s + Enmudecer o micrófono + Activar o micrófono + Mensaxes + Privacidade + Información persoal + Promover a moderador + Conversa pública + Desactivadas as notificacións emerxentes + Desculpe, algo foi mal, o erro é %1$s + Desculpe, algo foi mal,, non foi posíbel obter a mensaxe emerxente de proba + A notificación emerxente foi enviada satisfactoriamente. Agora ten que recibir unha notificación sobre este dispositivo co título «Probas de notificacións emerxentes» + Prema para falar + Co microfono desactivado, prema e manteña para usar a función «Prema para falar» + Lembrarmo más adiante + Retirar de favoritos + Retirar grupo e membros + Retirar participante + Retirar o contrasinal + Retirar equipo e membros + Cambiar o nome da conversa + Cambiar o nome + Responder + Responder en privado + A sala foi reservada satisfactoriamente + Gardar + Gardadao satisfactoriamente + 30 segundos + 5 minutos + 1 minuto + 10 minutos + Inmediato + 600 + 60 + 30 + 300 + Buscar + Limpar a busca + Seleccione unha conta + Actualizar a mensaxe + Enviar a gravación de voz + Conversa sensíbel + A vista previa da mensaxe estará desactivada na lista de conversas e nas notificacións + %1$s enviou un GIF. + Vde. enviou un GIF. + %1$s enviou un vídeo. + Vde. enviou un vídeo. + %1$s enviou un ficheiro de son. + Vde. enviou un ficheiro de son. + %1$s enviou unha imaxe. + Vde. enviou unha imaxe. + %1$s enviou unha tarxeta de Gabeta + Probar a conexión co servidor + Ten que anovar a base de datos %1$s + Produciuse un fallo ao importar a conta seleccionada + A ligazón á súa interface web %1$s cando a abre no navegador. + Importar unha conta desde a aplicación %1$s + Importar unha conta + Importar contas desde a aplicación%1$s + Importar contas + Retire a posición de matemento de %1$s + Ten que rematar a instalación de %1$s + Probando a conexión + O servidor non ten instalada a aplicación Parladoiro + Enderezo do servidor https://… + %1$s só funciona con %2$s 13 ou superior + Definir un novo contrasinal + Definir o contrasinal + Axustes + Actualizouse a súa conta xa existente en troques de crear unha nova + Avanzado + Aparencia + Chamadas + Póñase en contacto coa administración de + Abrir a pantalla de diagnóstico para comprobar os axustes ou crear un informe de fallos + Diagnóstico + Indícalle ao teclado que desactive a aprendizaxe personalizada (sen garantías) + Teclado de incógnito + Sen son + A aplicación Parladoiro non está instalada no servidor contra o que tentou autenticarse + Notificacións + As notificacións son rexeitadas + As notificacións están permitidas + Mensaxes + Emparellar os contactos en función do número de teléfono para integrar o atallo de Parladoiro na lista de teléfonos + Erro 429, demasiadas solicitudes + Pode definir o seu número de teléfono para que outros usuarios poidan atopalo + Introduza o número de teléfono + Número de teléfono non válido + O número de teléfono foi estabelecido correctamente + Número de teléfono + Integración do número de teléfono + Privacidade + Máquina do proxy + Contrasinal do proxy + Porto do proxy + Tipo de proxy + Nome de usuario do proxy + Compartir o meu estado de lectura e amosar o estado de lectura doutras persoas + Estado de lectura + Volver autorizar a conta + Retirar + Retirar a conta + Confirme a súa intención de retirar a conta actual. + Bloquear %1$s co bloqueo de pantalla de Android ou co método biométrico compatíbel + Tempo de inactividade para o bloqueo da pantalla + Bloqueo de pantalla + Evita capturas de pantalla na lista recente e dentro da aplicación + Seguranza da pantalla + A versión do servidor é moi antiga e non será compatíbel coa próxima edición! + A versión do servidor é demasiado antiga e non é compatíbel con esta versión da aplicación para Android + Servidor non admitido + A aplicación de notificacións do servidor non está instalada + Definido polo aforrador de batería + Escuro + Usar o predeterminado do sistema + tema + Claro + Tema + Compartir o meu estado de escritura e amosar o estado de escritura dos demais + O estado da escritura só está dispoñíbel cando se emprega unha infraestrutura de alto rendemento (HPB) + Estado da escritura + O proxy precisa credenciais + Advertencia + Só pode volver autorizarse a conta actual + Compartir o contacto + Precísase de permiso para ler a lista de contactos + Compartir a localización actual + Ligazón para compartir + Compartir a localización + Compartir esta localización + Escoller unha conta + Elementos compartidos + Tarxeta + Imaxes, ficheiros, mensaxes de voz… + Non hai elementos compartidos + Localización + Localización compartida + Cando as notificacións non estean configuradas correctamente, amosa unha advertencia periódica + Amosar a advertencia de notificación periódica + Ordenar por + Iniciar unha parola en grupo + Hora de comezo + Cambiar de conta + Equipo + Proba das notificacións emerxentes + Resultados da proba + Hoxe ás %1$s + Mañá ás %1$s + Escoller os ficheiros + Quere enviar estes ficheiros a %1$s? + Quere enviar este ficheiro a %1$s? + Desculpe, produciuse un fallo no envío + Produciuse un fallo ao enviar %1$s + Produciuse un fallo + Compartir desde %1$s + Enviar desde o dispositivo + Enviando + %1$s a %2$s - %3$s\%% + Tirar unha foto + Gravar un vídeo + Usuario + Gravación de vídeo de %1$s + Gravación da conversa de %1$s (%2$s) + Manteña premido para gravar, solte para enviar. + Precísase de permiso para gravar son + « Esvare para cancelar + Seminario web + Si + Semana seguinte + Non hai conversas arquivadas + Non se gardou ningunha mensaxe sen conexión + Non hai integración do número de teléfono por mor da falta de permisos + Todas as mensaxes + Só as mencións con \@ + Apagado + Predeterminado + Seguir os axustes da conversa + 1 hora + En liña + Estado en liña + Conversas abertas + Abrir na aplicación de Ficheiros + Abrir Notas + Ir ao fío + Reproducir/poñer en pausa a mensaxe de voz + Control da velocidade de reprodución + Engadir unha opción + Editar o voto + Finalizar a enquisa + Confirma que quere rematar esta enquisa? Isto non pode desfacerse. + Non é posíbel votar con máis opcións para esta enquisa. + Varias respostas + Eliminar a opción %1$d + Opción %1$d + Opcións + Enquisa privada + Pregunta + A súa pregunta + Resultados + Axustes + Votar + Voto enviado + Estabelecido previamente + Non foi posíbel ler o código QR. + Erguer a man + Todo + Non é posible compartir ficheiros desde o almacenamento sen permisos + Fíos recentes + Estase a gravar a chamada + Cancelar o inicio da gravación + Produciuse un fallo na gravación. Póñase en contacto coa administración desta instancia. + Comezar a gravar + Confirma que quere deter a gravación? + Deter a gravación da chamada + Deter a gravación + Deter a gravación + O consentimento de gravación é necesario para todas as chamadas + A gravación pode incluír a súa voz, o vídeo da cámara e da pantalla compartida. É preciso o seu consentimento antes de unirse á chamada. Consinte? + Esixir o consentimento de gravación antes de unirse á chamada nesta conversa + Consentimento de gravación + É posíbel que a chamada sexa gravada. + Gravando + Retirouse a conversa %1$s dos favoritos + Cambióuselle o nome a conversa %1$s + Volver enviar + Restabelecer o estado + Non é posible unirse a outras salas mentres está nunha chamada + Gardar + Scan QR Code + Sincronizar só con servidores de confianza + Federado + Visíbel só para as persoas desta instancia e os convidados + Local + Só son visíbeis para as persoas coincidentes mediante a integración do número de teléfono a través de Parladoiro no móbil + Privado + Sincronizar con servidores de confianza e co caderno de enderezos global e público + Publicado + Cambiar o ámbito + Cambiar o nivel de privacidade de %1$s + Desprazarse ata o final + Icona «Buscar» + segundos atrás + Seleccionado + Enviar o correo + Enviar a + Vde. non ten permiso para compartir contido nesta parola + Enviar a… + Enviar sen notificación + Definir + Definir o avatar desde a cámara + Definir o estado + Definir a mensaxe de estado + Compartir + Únase á conversa %1$s en %2$s + Son + Ficheiro + Multimedia + Outro + Enquisa + Gravación da chamada + Voz + Amosar o motivo da expulsión + Amosar os participantes expulsados + Favorito + Vde. non ten permiso para iniciar unha chamada + Crear un fío + iniciar unha chamada + Mensaxe de estado + Estado revertido + Cambiar á sala parcial + Cambiar á sala principal + Tirar unha foto + Produciuse un erro ao tirar a foto + Non é posible tirar unha foto sen permisos + Volver tirar a foto + Enviar + Cambiar de cámara + Recortar a foto + Reducir o tamaño da imaxe + Alternar o facho + 30 minutos + Esta semana + Esta é unha mensaxe de proba + Este fin de semana + Cancelar a creación do fío + Notificacións de fíos + Responder + Título do fío + Non se atopou ningún fío + Hoxe + Mañá + Traducir + Tradución + Copiar o texto traducido + Detectar o idioma + Axustes do dispositivo + Non foi posíbel detectar o idioma + Produciuse un fallo na tradución + Desde: + Para + e 1 máis está a escribir… + están a escribiren… + está a escribir… + e %1$s máis están a escribiren… + Desarquivar a conversa + Cando se desarquiva unha conversa, de xeito predeterminado esta volverá ser amosada. + %1$s foi desarquivada + Retirar a expulsión + Sen ler + Enviar un novo avatar desde o dispositivo + %1$s está fóra da oficina e é posíbel que non responda + %1$s hoxe está fóra da oficina + Substitución: + Avatar do usuario + Enderezo + Nome completo + Correo-e + Número de teléfono + Twitter + Sitio web + Estado + Produciuse un erro ao recuperar a información persoal do usuario. + Non foi estabelecida a información persoal + Engada o seu nome, imaxe e detalles de contacto na súa páxina de perfil. + Vídeo chamada + Cal é o seu estado? + + Ver %d mensaxe semellante + Ver %d mensaxes semellantes + + + Esta conversa eliminarase automaticamente para todos con %1$d día sen actividade. + Esta conversa eliminarase automaticamente para todos aos %1$d días sen actividade. + + + %d resposta + %d respostas + + + %d voto + %d votos + + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml new file mode 100644 index 0000000..e6ea335 --- /dev/null +++ b/app/src/main/res/values-hr/strings.xml @@ -0,0 +1,408 @@ + + + Uredi + Dodaj + Traži u %s + Arhivirano + Telefon + Zvučnik + Avatar + Odsutan + Zauzeto + Kalendar + Odaberi avatar iz oblaka + Izbriši poruku statusa + Izbriši poruku statusa nakon + Zatvori + Veza je uspostavljena + Razgovori + Stvori razgovor + Prilagođeno + Zona opasnosti + Izbriši avatar + Ne ometaj + Ne briši + Uredi + Nedavni + Šifrirano + Spremanje %1$s nije uspjelo + 15 minuta + mapa + Učitavanje… + %1$s (%2$d) + 4 sata + Nevidljiva + Napusti poziv + Učitaj više rezultata + Zaključaj razgovor + Simbol zaključavanja + Spusti ruku + Najnoviji prvi + Najstariji prvi + A – Z + Z – A + Najveći prvi + Najmanji prvi + Poruku ste vi izbrisali + Nema rezultata pretraživanja + Počnite unositi tekst za pretraživanje … + Traži … + Poruke + Odabrani račun sad je uvezen i dostupan + Informacije + Aktivni korisnik + Dodaj račun + Račun je zakazan za brisanje i ne može se promijeniti + Otvori glavni izbornik + Dodaj privitak + Dodaj emojije + Dodaj u razgovor + Dodaj sudionike + Dodaj u favorite + U redu, završeno! + Prikvači: %1$s + Otključaj %1$s + Poklopi + DOLAZNI + Naziv razgovora + Ponovno povezivanje … + ZVONI + %1$s u pozivu + %1$s putem telefona + %1$s putem videozapisa + Nema odgovora nakon 45 sekundi, dodirnite za ponovni pokušaj + %s poziv + %s videopoziv + %s glasovni poziv + Odustani + Neuspješno dohvaćanje mogućnosti, prekid + Vjerujete li dosad nepoznatoj SSL vjerodajnici koju je izdao %1$s za %2$s i koja vrijedi od %3$s do %4$s? + Pogledaj vjerodajnicu + Vezu je onemogućila vaša postavka SSL-a + Promijeni vjerodajnicu za autentifikaciju + Promijeni zaporku + Izbriši sve poruke + Izbrisane su sve poruke + Želite li zaista izbrisati sve poruke u ovom razgovoru? + Promijeni klijentsku vjerodajnicu + Postavi klijentsku vjerodajnicu + i + Kopirajte + Kopirano u međuspremnik + Stvori + Onemogućeno + Zanemari + Oprostite, nešto je pošlo po krivu! + Postavi + Preskoči + Nepoznata pogreška + Odaberi vjerodajnicu za autentifikaciju + Povezivanje… + Gotovo + Informacije o razgovoru + Videopoziv + Glasovni poziv + Postavke razgovora + Pridružite se razgovoru ili započnite novi + Pozdravi svoje prijatelje i kolege! + Kopiraj + Danas + Jučer + Izbriši + Izbriši sve + Izbriši razgovor + Ako izbrišete razgovor, također će biti izbrisan za sve ostale sudionike. + Izbriši poruku + Poruka je uspješno izbrisana, ali je možda prenesena u druge usluge + Ukloni moderatora + Snimi glasovnu poruku + Pošalji poruku + Trenutni račun + Poslužitelj + Inačica Androida + Aplikacija + Naziv aplikacije + Postavke baterije + Uređaj + Telefon + Vanjsko + Interno + Netočna zaporka + Ažuriraj + Želite li ponovno autorizirati ili izbrisati ovaj račun? + Ne + Da + Neuspješno dohvaćanje imena za prikaz, prekid + Nije moguće pohraniti ime za prikaz, prekid + Uredi + Uredi + Isključeno + 1 dan + 1 sat + 1 tjedan + Neuspješno dohvaćanje postavki signaliziranja + Prihvati + Odbij + Natrag + Korisnik slijedi javnu poveznicu + Vi: %1$s + Proslijedi + Proslijedi… + Galerija + Nemate poslužitelj?\nKliknite ovdje kako biste ga dobili od davatelja usluge + Preuzmi izvorni kod + Grupa + Gost + Dopusti pristup gostima + Unesite zaporku + Postavite zaporku za ograničavanje uporabe javne poveznice. + Zaštita zaporkom + Ponovno pošalji pozivnice + Dijeli poveznicu za razgovor + Unesi poruku … + Važan razgovor + Pozivnice + %1$s | Posljednja izmjena: %2$s + Napusti razgovor + Napuštanje poziva … + Opća javna licenca za GNU, verzija 3. + Licenca + Dostignuto je ograničenje od %s znakova + Predvorje + Sastanak počinje u %1$s + Sastanak počinje uskoro + Trenutno čekate u predvorju. + Vaša trenutačna lokacija + potrebna je dozvola za lokaciju + Nepoznat položaj + Zaključano + Dodirni za otključavanje + Nije postavljeno + Označi kao pročitano + Označi kao nepročitano + Slanje poruke nije uspjelo: + Prekini odgovor + Poruka pročitana + Poruka poslana + Moderator + Novi razgovor + Nepročitana spominjanja + Nove poruke + Gost + Ne + Nema proxyja + Ne sada + %1$s na kanalu za obavijesti %2$s + Pozivi + Poruke + Otpreme + Postavke obavijesti + Uvijek šalji obavijesti + Obavijesti kada se spomene + Nikad ne šalji obavijesti + Trenutno izvan mreže, provjerite vezu + U redu + Otvori razgovor registriranim korisnicima + Otvori i gostujućim korisnicima aplikacije + Vlasnik + Sudionici + Dodaj sudionike + Zaporka + Otvori postavke + Račun nije pronađen + Razmjenjujte poruke putem %s + Omogućite mikrofon + Poruke + Privatnost + Osobne informacije + Unaprijedi u moderatora + Push obavijesti su onemogućene + Pritisni za govor + Kada je mikrofon onemogućen, kliknite i držite za uporabu značajke Pritisni za govor + Podsjeti me kasnije + Ukloni iz favorita + Ukloni grupu i članove + Ukloni sudionika + Preimenuj razgovor + Preimenuj + Odgovori + Odgovori privatno + Spremi + Uspješno spremljeno + 30 sekundi + 5 minuta + 1 minuta + 10 minuta + 600 + 60 + 30 + 300 + Traži + Odaberi račun + %1$s je poslao GIF. + Poslali ste GIF. + %1$s je poslao videozapis. + Poslali ste videozapis. + %1$s je poslao zvučnu datoteku. + Poslali ste zvučnu datoteku. + %1$s je poslao sliku. + Poslali ste sliku. + Testirajte vezu s poslužiteljem + Nadogradite %1$s bazu podataka + Uvoz odabranog računa nije uspio + Poveznica do vašeg web-sučelja %1$s kada ga otvorite u pregledniku. + Uvezi račun iz %1$s aplikacije + Uvezi račun + Uvezi račune iz %1$s aplikacije + Uvezi račune + Prekinite održavanje za %1$s + Završite instalaciju %1$s + Ispitivanje veze + Na poslužitelju nije instalirana podržana aplikacija Talk + Adresa poslužitelja https://… + %1$s radi samo s %2$s 13 i novijom inačicom + Postavke + Vaš postojeći račun je ažuriran, umjesto dodavanja novog + Napredno + Izgled + Pozivi + Naređuje tipkovnici da onemogući personalizirano učenje (nije zajamčeno) + Anonimna tipkovnica + Bez zvuka + Aplikacija Talk nije instalirana na poslužitelju s kojim ste se pokušali autentificirati + Obavijesti + Poruke + Izvršite podudaranje kontakata na temelju telefonskog broja radi integracije prečaca aplikacije Talk u aplikaciju sustava za kontakte + Možete postaviti svoj telefonski broj kako bi vas drugi korisnici mogli pronaći + Unesite broj telefona + Nevažeći broj telefona + Broj telefona je uspješno postavljen + Broj telefona + Integracija broja telefona + Privatnost + Proxy računalo + Zaporka za proxy + Proxy port + Vrsta proxy poslužitelja + Korisničko ime za proxy + Dijeli moj status čitanja i prikaži statuse čitanja drugih sudionika + Status čitanja + Ponovno autoriziraj račun + Ukloni + Izbriši račun + Potvrdite namjeru uklanjanja predmetnog računa. + Zaključaj %1$s s pomoću značajke zaključavanja zaslona Android uređaja ili podržanom biometrijskom metodom + Istek vremena neaktivnosti zaključavanja zaslona + Zaključavanje zaslona + Sprječava snimke zaslona na nedavnom popisu i unutar aplikacije + Sigurnost zaslona + Inačica poslužitelja je vrlo stara i neće biti podržana u sljedećem izdanju! + Inačica poslužitelja je prestara i nije podržana u ovoj inačici aplikacije za Android + Nepodržan poslužitelj + Tamno + Koristi zadanu postavku sustava + tema + Svijetlo + Tema + Proxy zahtijeva vjerodajnice + Upozorenje + Samo se predmetni račun može ponovno autorizirati + Dijeli kontakt + Dijeli trenutačnu lokaciju + Dijeli poveznicu + Dijeli lokaciju + Dijeli ovu lokaciju + Odaberi račun + Deck kartica + Lokacija + Dijeljena lokacija + Razvrstaj prema + Vrijeme početka + Zamijeni račun + Odaberi datoteke + Poslati ove datoteke na %1$s? + Poslati ovu datoteku na %1$s? + Nažalost, otpremanje nije uspjelo + Dijeli iz %1$s + Otpremanje + Snimi fotografiju + Korisnik + Snimka iz aplikacije Talk od %1$s (%2$s) + Držite za snimanje, pustite za slanje. + Potrebno je dopuštenje za snimanje zvuka + « Pomakni za otkazivanje + Webinar + Da + Sljedeći tjedan + Integracija broja telefona nije moguća jer nedostaje dopuštenje + Zadani + 1 sat + Na mreži + Status na mreži + Otvori razgovore + Otvori u aplikaciji Files + Reproduciraj/pauziraj glasovnu poruku + Dodaj mogućnost + Mogućnosti + Rezultati + Postavke + Podigni ruku + Sve + Dijeljenje datoteka iz pohrane nije moguće bez dopuštenja + Snimanje + Spremi + Scan QR Code + Sinkroniziraj samo s pouzdanim poslužiteljima + Udruženo + Vidljivo samo ljudima na ovoj instanci i gostima + Lokalno + Vidljivo samo ljudima koji se podudaraju putem integracije broja telefona u aplikaciji Talk na mobilnom uređaju + Privatno + Sinkroniziraj s pouzdanim poslužiteljima i globalnim i javnim adresarom + Objavljeno + Uključi/isključi opseg + Promijeni razinu privatnosti za %1$s + Pomakni se na dno + prije par sekundi + Odabrano + Pošalji poruku e-pošte + Pošalji na + Pošalji na… + Postavi + Postavi status + Postavi poruku statusa + Dijeli + Audio + Datoteka + Medij + Drugo + Glas + Favorit + Poruka statusa + Snimi fotografiju + Pošalji + Promijeni kameru + Uključi/isključi svjetlo + 30 minuta + Ovaj tjedan + Danas + Sutra + Prevedi + Postavke uređaja + Od + Do + Otpremi novi avatar s uređaja + Avatar korisnika + Adresa + Puno ime + E-pošta + Broj telefona + Twitter + Web-mjesto + Status + Dohvaćanje osobnih korisničkih podataka nije uspjelo. + Nisu postavljeni osobni podaci + Dodajte ime, sliku i kontaktne podatke na stranicu profila. + Video poziv + Koji je vaš status? + diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml new file mode 100644 index 0000000..a9ed840 --- /dev/null +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -0,0 +1,701 @@ + + + Szerkesztés + Hozzáadás + Hozzáadás a jegyzetekhez + A(z) %1$s beszélgetés hozzáadva a kedvencekhez + Keresés itt: %s + Beszélgetés archiválása + Ha archivál egy beszélgetést, akkor alapértelmezetten el lesz rejtve. Válassza az „Archiválva” szűrőt az archivált beszélgetések megtekintéséhez. A közvetlen említéseket továbbra is meg fogja kapni. + Archiválva + Archiválva: %1$s + Hanghívás + Bluetooth + Hangkimenet + Telefon + Hangszóró + Vezetékes fejhallgató + Az állapota automatikusan lett beállítva + Profilkép + Távol + Vissza gomb + Tiltás + Résztvevő letiltása + Tiltólista + Foglalt + Naptár + Speciális hívásbeállítások + A hívás egy órája tart. + Hívás értesítés nélkül + Kamera engedély megadva. Válassza újra a kamerát. + Bejelentkezés megszakítása + Profilkép választása a felhőből + Állapotüzenet törlése + Állapotüzenet törlése ennyi idő után: + Bezárás + Bezárás ikon + A kapcsolat létrejött + Nincs kapcsolat a kiszolgálóval + A kapcsolat elveszett – Elküldött üzenetek sorba állítva + Felvétel zárolása a hangüzenet folyamatos rögzítéséhez + A beszélgetés archiválva van + A beszélgetés csak olvasható + Nem sikerült csak olvashatóvá állítani a beszélgetést + Beszélgetések + Beszélgetés létrehozása + Hibajegy létrehozása + Egyéni + Veszélyes területet + %1$s itt: %2$s + Profilkép törlése + %1$s beszélgetés törlése + Ne zavarjanak + Ne törölje + Szerkesztés + A 24 óránál régebbi beszélgetések nem szerkeszthetőek + Üzenet szerkesztése + Legutóbbiak + Titkosított + Hívás befejezése + Hívás befejezése mindenki számára + Hiba történt a csevegések betöltése során + Hiba történt a résztvevő tiltásának visszavonása során + Sikertelen mentés: %1$s + 15 perc + mappa + Betöltés… + %1$s (%2$d) + 4 óra + A függőben lévő meghívások lekérése sikertelen + (szerkesztve) + Belső jegyzet + Láthatatlan + A nyelveket nem lehet lekérni + Lekérés sikertelen + Mai nap később + Hívás elhagyása + Elhagyta a következő beszélgetést: %1$s + További találatok betöltése + Helyi idő: %1$s + Beszélgetés zárolása + Zár szimbólum + Kéz letétele + %1$s beszélgetés olvasottként jelölése + %1$s beszélgetés olvasatlanként jelölése + Említett + Legújabb elöl + Régebbiek elöl + A – Z + Z – A + Legnagyobb elöl + Legkisebb elöl + Üzenet másolva + Törölte az üzenetet + Szerkesztette: %1$s + Koppintson a szavazás megnyitásához + Nincs találat + Kezdjen el gépelni a kereséshez… + Keresés… + Üzenetek + A kiválasztott fiók importálva lett és elérhető + Leírás + Aktív felhasználó + Fiók hozzáadása + A fiók törlésre van jelölve, ezért nem lehet módosítani + Főmenü megnyitása + Melléklet hozzáadása + Emodzsi hozzáadása + Hozzáadás a beszélgetéshez + Résztvevők hozzáadása + Hozzáadás a kedvencekhez + Rendben, minden kész! + PIN: %1$s + %1$s feloldása + A bluetooth-os hangszórók engedélyezéséhez adja meg a „Közeli eszközök” engedélyt. + Válasz videóhívásként + Válasz csak hanghívásként + Hangkimenet módosítása + Kamera be/ki + Hívás letétele + Mikrofon be/ki + Megnyitás kép a képben módban + Váltás a saját videóra + BEJÖVŐ + Beszélgetésnév + Hívásértesítések + %1$s felemelte a kezét + Újrakapcsolódás… + CSENGETÉS + %1$s hívásban + %1$s telefonnal + %1$s videóval + Nincs válasz 45 másodpercen belül, koppintson az újrapróbálkozáshoz + %s hívás + %s videóhívás + %s hanghívás + A videóhívás engedélyezéséhez adja meg a „Kamera” engedélyt. + Mégse + A lehetőségek lekérdezése sikertelen, megszakítás + Felirat + Megbízik a(z) %1$s által a(z) %2$s részére kiállított, %3$s és %4$s között érvényes, korábban ismeretlen SSL tanúsítványban? + Ellenőrizze a tanúsítványt + Az SSL beállítás megakadályozta a kapcsolódást + Hitelesítési tanúsítvány módosítása + Jelszó módosítása + Szerkesztés megszakítása + Szerkesztés megszakítása + Összes üzenet törlése + Az összes üzenet törölve lett + Biztos, hogy törli a beszélgetés összes üzenetét? + Klienstanúsítvány módosítása + Klienstanúsítvány beállítása + és + Másolás + Vágólapra másolva + Létrehozás + Letiltva + Elutasítás + Valami hiba történt! + További lehetőségek + Beállítás + Kihagyás + Ismeretlen + Hitelesítési tanúsítvány kiválasztása + Csatlakozás… + Kész + Beszélgetés leírása + Beszélgetés információk + Videóhívás + Hanghívás + Beszélgetés nem található + Beszélgetésbeállítások + Csatlakozzon egy beszélgetéshez, vagy indítson egy újat + Üdvözölje a barátait és munkatársait! + Másolás + Új beszélgetés létrehozása + Szavazás létrehozása + Ön: + Ma + Tegnap + Törlés + Összes törlése + Beszélgetés törlése + Ha törli a beszélgetést, akkor az összes többi résztvevő számára is törölve lesz. + Üzenet törlése + Az üzenet törlése sikeresen megtörtént, de lehet, hogy az már megjelent más szolgáltatásokon + Törlés most + %1$s felhasználó el lett távolítva + Lefokozás moderátorról + Hangüzenet felvétele + Üzenet küldése + Jelenlegi fiók + Kiszolgáló + A kiszolgálóértesítések alkalmazása telepítve van? + Felhasználó + A felhasználói állapot engedélyezett? + Android verzió + Alkalmazás + Alkalmazásnév + Regisztrált felhasználók + Alkalmazásverzió + Az akkumulátoroptimalizálás figyelmen kívül hagyva, minden rendben + Az akkumulátoroptimalizálás engedélyezve van, amely problémákat okozhat. Ajánlatos letiltani az akkumulátoroptimalizálást. + Akkumulátorbeállítások + Eszköz + Hibaelhárítási ellenőrzőlista megnyitása + Diagnosztikai képernyő megnyitása + A dontkillmyapp.com megnyitása + Firebase leküldéses token legfrissebb lekérése + Firebase leküldéses token legfrissebb előállítása + Még nincs Firebase leküldéses token beállítva. Hozzon létre egy hibajelentést. + Firebase leküldéses token + A Google Play szolgáltatások nem érhetőek el. Az értesítések nem támogatottak. + Google Play szolgáltatások + A Google Play szolgáltatások elérhetőek + Legfrissebb leküldéses regisztráció a leküldési proxynál + Még nincs regisztrálva a leküldési proxynál + Legfrissebb leküldéses regisztráció a kiszolgálónál + Még nincs regisztrálva a kiszolgálónál + Metainformációk + Rendszerjelentés előállítása + Engedélyezve van a hívások értesítési csatornája? + Engedélyezve van az üzenetek értesítési csatornája? + Értesítési engedély + Telefon + Kiszolgáló Beszélgetés verziója + Kiszolgáló verziója + Külső + Belső + Jelzőkiszolgáló módja + Érvénytelen jelszó + A kiszolgáló jelenleg karbantartási módban van. + Az alkalmazás elavult + Az alkalmazás túl régi, és már nem támogatja ez a kiszolgáló. Frissítse. + Frissítés + Újrahitelesíti vagy törli ezt a fiókot? + A média tárhelyre mentése lehetővé teszi, hogy az eszközön lévő más alkalmazások is hozzáférjenek. + Folytatja? + Nem + Menti a tárolóba? + Igen + A megjelenítendő név nem kérhető le, megszakítás + A megjelenítendő név nem tárolható, megszakítás + Szerkesztés + Szerkesztés + Üzenet szerkesztése + Rendszergazda által szerkesztve + Esemény beszélgetési menüje + Ütemterv + 8 óra + 4 hét + Ki + 1 nap + 1 óra + 1 hét + Csevegési üzenetek elévülése + A csevegési üzenetek bizonyos idő után elévülhetnek. Megjegyzés: A csevegésben megosztott fájlok nem törlődnek a tulajdonosuk számára, csak többé nem lesznek megosztva a beszélgetéssel. + A jelzőkiszolgáló beállításainak lekérése sikertelen + Elfogadás + Elutasítás + ettől: %1$s, ekkor: %2$s + Nincs függőben lévő meghívás + Függőben lévő meghívásai vannak + Vissza + Fájlelérési jogosultság szükséges + Beszélgetések szűrése + Nyilvános hivatkozást használó felhasználó + Ön: %1$s + Továbbítás + Továbbítás… + Galéria + Nincs még saját kiszolgálója?\nKattintson ide a beszerzéshez az egyik szolgáltatótól + Forráskód letöltése + Csoport + Vendég + Vendéghozzáférés + A vendéghozzáférés nem kapcsolható be/ki. + A vendégek nyilvános hivatkozásokat oszthatnak meg a beszélgetéshez való csatlakozáshoz. + Vendégek engedélyezése + Adja meg a jelszót + Vendéghozzáférés jelszava + Hiba a jelszó beállítása/kikapcsolása során. + Jelszó beállítása annak korlátozásához, hogy kik használhassák a nyilvános hivatkozást. + Jelszavas védelem + Meghívások újraküldése + A meghívások egy hiba miatt nem lettek elküldve. + A meghívások újra ki lettek küldve. + Beszélgetés hivatkozásának megosztása + Írjon üzenetet… + Az akkumulátoroptimalizálás nincs mellőzve. Ezt módosítania kell, hogy biztosan működjenek az értesítések a háttérben! Kattintson az OK gombra, és válassza az „Összes alkalmazás” -> %1$s -> Ne optimalizálja lehetőséget. + Akkumulátoroptimalizálás mellőzése + Fontos beszélgetés + A „Ne zavarjanak” felhasználói állapot figyelmen kívül hagyása a fontos beszélgetéseknél + Érvénytelen idő + Meghívások + Csatlakozás a nyílt beszélgetésekhez + Megtartás + Új moderátort kell kineveznie, mielőtt elhagyhatja a beszélgetést + %1$s | Utoljára módosítva: %2$s + Beszélgetés elhagyása + Hívás elhagyása… + GNU General Public License, Version 3 + Licenc + %s karakteres korlát elérve + Váró + A találkozó ekkorra van ütemezve: %1$s + Ez a találkozó hamarosan elkezdődik + Jelenleg a váróban van. + A jelenlegi helye + hely engedély szükséges + Ismeretlen pozíci + Zárolt + Koppintson a feloldáshoz + Nincs megadva + Megjelölés olvasottként + Megjelölés olvasatlanként + Beszélgetés fontosnak jelölve + Beszélgetés érzékenynek jelölésének visszavonása + Beszélgetés érzékenynek jelölve + Beszélgetés fontosnak jelölésének visszavonása + A találkozó véget ért + Üzenet hozzáadva a jegyzetekhez + Sikertelen + Nem sikerült elküldeni az üzenetet: + Nincs kapcsolat + Válasz elvetése + Üzenet elolvasva + Küldés + Üzenet elküldve + A hanghívás engedélyezéséhez meg kell adnia a „Mikrofon” engedélyt. + Nem fogadott hívás a következőtől: %s + Moderátor + Új beszélgetés + Láthatóság + Olvasatlan említések + Olvasatlan üzenetek + A(z) %1$s nem érhető el (nincs telepítve vagy egy rendszergazda korlátozta) + Vendég + Nem + Nincs nyitott beszélgetés + Nincs nyitott beszélgetés, melyhez csatlakozhatna.\nVagy nincsenek nyitott beszélgetések, vagy már mindhez csatlakozott. + Nincs proxy + Nem kapcsolhatja be a hangot. + Nem kapcsolhatja be a videót. + Most nem + %1$s a(z) %2$s értesítési csatornán + Hívások + Értesítés a bejövő hívásokról + Üzenetek + Értesítés a bejövő üzenetekről + Feltöltések + Értesítés a feltöltési folyamatról + Értesítési beállítások + Az értesítések nincsenek helyesen beállítva + Az értesítési és akkumulátoroptimalizálási beállítások helyesen vannak beállítva az értesítések fogadásához. Ha így is problémái vannak vele, akkor ellenőrizze, hogy az hívások és üzenetek értesítési csatornái engedélyezve vannak-e. További súgó a DontKillMyApp.com weboldalon vagy a hibaelhárítási ellenőrzőlistán található. Ha ez nem segít, akkor ugorjon a diagnosztikai képernyőre, és küldjön hibajelentést. + Értesítések hibaelhárítása + Mindig értesítsen + Értesítsen, ha megemlítik Önt + Sose értesítsen + Jelenleg offline, ellenőrizze a kapcsolatát + OK + Jelenleg tartó találkozó + Beszélgetés megnyitása a regisztrált felhasználók számára + Megnyitás a vendégfelhasználók számára is + Tulajdonos + Résztvevők + Résztvevők hozzáadása + Jelszó + Engedélyek beállítása + Néhány engedély meg lett tagadva. + Adja meg az engedélyeket + Beállítások megnyitása + Adjon engedélyt a Beállítások > Engedélyek menüpontban + A fiók nem található + Csevegés %s segítségével + Mikrofon némítása + Mikrofon engedélyezése + Üzenetek + Adatvédelem + Személyes információk + Kinevezés moderátorrá + Nyilvános beszélgetés + Leküldéses értesítések kikapcsolva + Sajnos valamilyen hiba történt, a hiba: %1$s + Sajnos valamilyen hiba történt, a leküldéses tesztüzenet nem kérhető le + A leküldéses értesítés sikeresen elküldve. Kapnia kellene egy „Leküldéses értesítések tesztelése” című üzenetet ezen az eszközön. + Lenyomás a beszédhez + A mikrofon le lesz tiltva, a beszédhez tartsa lenyomva + Emlékeztessen később + Eltávolítás a kedvencekből + Csoport és tagok eltávolítása + Résztvevő eltávolítása + Jelszó eltávolítása + Csapat és tagok eltávolítása + Beszélgetés átnevezése + Átnevezés + Válasz + Válasz privátban + A szoba sikeresen megtartva + Mentés + Sikeres mentés + 30 másodperc + 5 perc + 1 perc + 10 perc + Azonnal + 600 + 60 + 30 + 300 + Keresés + Keresés törlése + Fiók kiválasztása + Üzenet frissítése + Érzékeny beszélgetés + Az üzenet-előnézet le lesz tiltva a beszélgetési listában és az értesítésekben + %1$s GIF képet küldött. + Ön GIF képet küldött. + %1$s videót küldött. + Ön videót küldött. + %1$s hangot küldött. + Ön hangot küldött. + %1$s képet küldött. + Ön képet küldött. + %1$s kártyát küldött + Kiszolgálókapcsolat tesztelése + Frissítse a(z) %1$s adatbázisát + A kiválasztott fiók importálása sikertelen + A %1$s webes felületére mutató hivatkozás, amikor megnyitja a böngészőben. + Fiók importálása a(z) %1$s alkalmazásból + Fiók importálása + Fiókok importálása a(z) %1$s alkalmazásból + Fiókok importálása + Hozza ki a(z) %1$s kiszolgálóját karbantartási módból + Fejezze be a(z) %1$s telepítését + Kapcsolat ellenőrzése + A kiszolgálón nincs támogatott Beszélgetés alkalmazás telepítve + Kiszolgálócím https://… + A(z) %1$s csak %2$s 13 vagy újabb verzióval működik + Jelszó beállítása + Jelszó megadása + Beállítások + Új fiók hozzáadása helyett, a már létező fiókja lett frissítve + Speciális + Megjelenés + Hívások + Lépjen kapcsolatba a következő rendszergazdájával: + Nyissa meg a diagnosztikai képernyőt a beállítások ellenőrzéséhez vagy a hiba jelentéséhez + Diagnózis + Arra utasítja a billentyűzetet, hogy kapcsolja ki a személyre szabott tanulást (nem garantált) + Inkognító billentyűzet + Nincs hang + A Beszélgetés alkalmazás nincs telepítve azon a kiszolgálón, ami felé hitelesíteni szeretne + Értesítések + Értesítések elutasítva + Értesítések engedélyezve + Üzenetek + Párosítsa a névjegyeket telefonszám alapján, hogy integrálja a Beszélgetéseket a rendszer Névjegyek alkalmazásba + 429-es hiba – Túl sok kérés + Beállíthatja telefonszámát, hogy a többi felhasználó megtalálja Önt + Írja be a telefonszámot + Érvénytelen telefonszám-formátum + Telefonszám beállítása sikeresen megtörtént + Telefonszám + Telefonszám integráció + Adatvédelem + Proxy kiszolgáló + Proxy jelszava + Proxy port + Proxy típusa + Proxy felhasználóneve + Saját olvasási állapot megosztása, és a mások olvasási állapotának megjelenítése + Olvasási állapot + Fiók újrahitelesítése + Eltávolítás + Fiók eltávolítása + Erősítse meg szándékát, hogy eltávolítja a jelenlegi fiókot. + A(z) %1$s zárolása az androidos képernyőzárral, vagy támogatott biometrikus eljárással + Képernyő zárolása ennyi tétlenség után + Képernyőzár + Megakadályozza a képernyőképek készítését az alkalmazáson belül és a nemrég használt alkalmazások közt + Képernyőbiztonság + A kiszolgáló verziója nagyon régi, és nem lesz támogatott a következő kiadásban! + A kiszolgáló verziója túl régi, és az androidos alkalmazás ezen verziója nem támogatja + Nem támogatott kiszolgáló + A kiszolgálóértesítések alkalmazása nincs telepítve + Akkumulátorkímélő által beállítva + Sötét + Rendszer alapértelmezésének használata + téma + Világos + Téma + Gépelési állapot megosztása, és a mások gépelési állapotának megjelenítése + A gépelési állapot csak a nagy teljesítményű háttérrendszer (HPB) használata esetén érhető el. + Gépelési állapot + A proxyhoz hitelesítő adatok szükségesek + Figyelmeztetés + Csak az aktuális fiókot lehet újraengedélyezni + Névjegy megosztása + A névjegyek olvasási engedélye szükséges + Jelenlegi hely megosztása + Megosztási hivatkozás + Hely megosztása + Ezen hely megosztása + Válasszon fiókot + Megosztott elemek + Kártya + Képek, fájlok, hangüzenetek… + Nincsenek megosztott elemek + Hely + Hely megosztva + Szokásos figyelmeztetés megjelenítése, ha az értesítések nincsenek helyesen beállítva + Szokásos értesítési figyelmeztetés megjelenítése + Rendezés elve + Csoportos csevegés indítása + Kezdési idő + Fiókváltás + Csapat + Leküldéses értesítések tesztelése + Teszteredmények + Ma ekkor: %1$s + Holnap ekkor: %1$s + Válasszon fájlokat + Fájlok küldése ide: %1$s? + Fájl küldése ide: %1$s? + A feltöltés sikertelen + A(z) %1$s feltöltése sikertelen + Sikertelen + Megosztás innen: %1$s + Feltöltés az eszközről + Feltöltés + %1$s → %2$s – %3$s\%% + Fénykép készítése + Videó készítése + Felhasználó + Videófelvétel innen: %1$s + Beszédfelvétel innen: %1$s (%2$s) + Tartsa a felvételhez, engedje el a küldéshez. + Hangfelvételi engedély szükséges + « Csúsztassa a megszakításhoz + Webinár + Igen + Következő hét + Nincs archivált beszélgetés + Nincs mentett offline üzenet + A hiányzó engedélyek miatt nincs telefonszám-integráció + Minden üzenet + csak @-megemlítések + Alapértelmezett + 1 óra + Elérhető + Elérhető állapot + Beszélgetések megnyitása + Megnyitás a Fájlok alkalmazásban + Jegyzetek megnyitása + Hangüzenet lejátszása/szüneteltetése + Lejátszási sebesség vezérlése + Lehetőség hozzáadása + Szavazat szerkesztése + Szavazás befejezése + Biztos, hogy befejezi ezt a szavazást? Ez nem vonható vissza. + Nem szavazhat több lehetőségre ennél a szavazásnál. + Több válasz + %1$d. lehetőség törlése + %1$d. lehetőség + Lehetőségek + Privát szavazás + Kérdés + Az Ön kérdése + Eredmények + Beállítások + Szavazat leadása + Szavazat leadva + Előzőleg beállított + Kéz felemelése + Összes + A fájlok megosztása a tárhelyről engedély nélkül nem lehetséges + A hívásról felvétel készül + Felvétel indításának megszakítása + A felvétel sikertelen. Lépjen kapcsolatba a rendszergazdával. + Felvétel indítása + Biztos, hogy le akarja állítani a felvételt? + Hívásfelvétel leállítása + Felvétel leállítása + Felvétel leállítása… + Beleegyezés szükséges az összes hívás rögzítéséhez + A rögzítés tartalmazhatja a hangját, kameravideóját és a képernyőmegosztását. A beleegyezése szükséges a híváshoz való csatlakozás előtt. Beleegyezik? + Beleegyezés megkövetelése a beszélgetés hívásaihoz való csatlakozás előtt + Rögzítési hozzájárulás + A hívásról felvétel készülhet. + Felvétel + A(z) %1$s beszélgetés eltávolítva a kedvencek közül + %1$s beszélgetés át lett nevezve + Újraküldés + Állapot visszaállítása + Hívás közben nem lehet más szobákhoz csatlakozni + Mentés + Scan QR Code + Szinkronizálás csak a megbízható kiszolgálókkal + Föderált + Csak az ezen a példányon lévő személyek és a vendégek láthatják + Helyi + Csak a mobilos Beszélgetés alkalmazáson keresztüli telefonszám integrációval egyeztetett emberek láthatják + Privát + Szinkronizálás a megbízható kiszolgálókkal, valamint a globális és nyilvános címjegyzékkel + Közzétett + Hatókör váltása + A(z) %1$s adatvédelmi szintjének módosítása + Görgetés az aljára + Keresési ikon + pár másodperce + Kiválasztott + E-mail küldése + Küldés + Nincs jogosultsága, hogy tartalmat osszon meg ebben a csevegésben + Küldés… + Küldés értesítés nélkül + Beállítás + Profilkép beállítása a kamerával + Állapot beállítása + Állapotüzenet beállítása + Megosztás + Csatlakozás a(z) %1$s beszélgetéshez ekkor: %2$s + Hangok + Fájl + Média + Egyebek + Szavazás + Hívásfelvétel + Hang + Kitiltás okának megjelenítése + Kitiltott résztvevők megjelenítése + Kedvenc + Nincs jogosultsága hívást indítani + hívás indítás + Állapotüzenet + Üzenet visszaállítva + Szétbontott szobára váltás + Fő szobára váltás + Fénykép készítése + Hiba a fénykép készítése során + Engedély nélkül a fénykép készítése nem lehetséges + Fénykép újbóli elkészítése + Küldés + Kamera váltása + Fénykép levágása + Képméret csökkentése + Lámpa be/ki + 30 perc + Ez a hét + Ez egy tesztüzenet + Ezen a hétvégén + Ma + Holnap + Lefordítás + Fordítás + Lefordított szöveg másolása + Nyelv észlelése + Eszközbeállítások + Nem sikerült észlelni a nyelvet + A fordítás sikertelen + Feladó + Címzett + és még 1 felhasználó gépel… + gépelnek… + gépel… + és még %1$s felhasználó gépel… + Beszélgetés archiválásának visszavonása + Ha egy beszélgetés archiválása vissza lesz vonva, akkor újból alapértelmezetten látszódni fog. + A(z) %1$s archiválása visszavonva + Kitiltás visszavonása + Olvasatlan + Új profilkép feltöltése az eszközről + %1$s házon kívül van, és lehet, hogy nem válaszol + %1$s ma házon kívül van + Helyettes: + Felhasználói profilkép + Cím + Teljes név + E-mail + Telefonszám + Twitter + Weboldal + Állapot + Nem sikerült beolvasni a személyes felhasználói adatokat. + Nincs személyes információ beállítva + Adja meg a nevét, profilképét és kapcsolati adatait a profil oldalán. + Videóhívás + Mi az állapota? + + %d hasonló üzenet megtekintése + %d hasonló üzenet megtekintése + + + Ez a beszélgetés %1$d nap tétlenség után mindenkinél törölve lesz + Ez a beszélgetés %1$d nap tétlenség után mindenkinél törölve lesz + + + %d szavazat + %d szavazat + + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 0000000..8d7399b --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,487 @@ + + + Modifica + Aggiungi + Cerca in %s + Appari non in linea + Archivia conversazione + Archiviati + Archiviato %1$s + Bluetooth + Uscita audio + Telefono + Altoparlante + Auricolare cablato + Stato impostato automaticamente + Avatar + Assente + Occupato + Calendario + Opzioni avanzate per le chiamate + Chiama senza notifica + Autorizzazione fotocamera concessa. Scegli di nuovo la fotocamera. + Annulla l\'accesso + Scegli avatar dalla rete + Cancella il messaggio di stato + Cancella il messaggio di stato dopo + Chiudi + Connessione stabilita + Conversazioni + Crea conversazione + Personalizzato + Zona pericolosa + Elimina avatar + Non disturbare + Non cancellare + Modifica + Modifica messaggio + Recenti + Cifrato + Si è verificato un problema durante il caricamento delle tue chat + Salvataggio di %1$s fallito + 15 minuti + cartella + Caricamento … + %1$s (%2$d) + Argomenti seguiti + 4 ore + Invisibile + Più tardi oggi + Lascia la chiamata + Mostra altri risultati + Blocca conversazione + Simbolo lucchetto + Abbassa la mano + Prima i più recenti + Prima i più datati + A - Z + Z - A + Prima i più grandi + Prima i più piccoli + Messaggio eliminato da te + Nessun risultato di ricerca + Inizia a digitare per cercare … + Cerca … + Messaggi + L\'account selezionato è ora importato e disponibile + Informazioni + Utente attivo + Aggiungi account + L\'eliminazione dell\'account è stata pianificata, e non può essere modificata + Apri menu principale + Aggiungi allegato + Aggiungi emoji + Aggiungi a conversazione + Aggiungi partecipanti + Aggiungi ai preferiti + OK, tutto fatto! + Appunta: %1$s + Sblocca %1$s + Rispondi come videochiamata + Rispondi solo come chiamata vocale + Cambia l\'uscita audio + Riaggancia + IN ARRIVO + Nome della conversazione + Notifiche di chiamata + Riconnessione in corso … + STA SQUILLANDO + %1$s in chiamata + %1$s con telefono + %1$s con video + Nessuna risposta in 45 secondi, tocca per provare ancora + %s chiamata + %s videochiamata + %s chiamata vocale + Annulla + Recupero delle capacità non riuscito, interruzione in corso + Ti fidi del certificato SSL fino ad ora sconosciuto, rilasciato da %1$s per %2$s, valido da %3$s a %4$s? + Controlla il certificato + La tua configurazione SSL ha impedito la connessione + Cambia certificato di autenticazione + Cambia password + Elimina tutti i messaggi + Tutti i messaggi sono stati eliminati + Vuoi davvero eliminare tutti i messaggi in questa conversazione? + Cambia certificato client + Configura certificato client + e + Copia + Copiato negli appunti + Crea + Disabilitata + Annulla + Spiacenti, qualcosa non ha funzionato! + Più opzioni + Imposta + Salta + Sconosciuto + Seleziona certificato di autenticazione + Connessione in corso… + Fine + Informazioni di conversazione + Chiamata video + Chiamata vocale + Impostazioni conversazione + Unisciti a una conversazione o iniziane una nuova + Saluta i tuoi amici e i tuoi colleghi! + Copia + Crea una nuova conversazione + Crea sondaggio + Oggi + Ieri + Elimina + Elimina tutto + Elimina conversazione + Se elimini la conversazione, sarà eliminata anche per tutti i partecipanti. + Elimina messaggio + Messaggio eliminato correttamente, ma potrebbe essere stato distribuito ad altri servizi + Declassa da moderatore + Registra messaggio vocale + Invia messaggio + Account attuale + Server + Utente + Versione Android + Applicazione + Nome applicazione + Impostazioni batteria + Dispositivo + Telefono + Versione del server Talk + Esterno + Interni + Password non valida + Aggiorna + Vuoi autorizzare nuovamente o eliminare questo account? + No + + Il nome visualizzato non può essere recuperato, interruzione in corso + Nome visualizzato non memorizzato, interruzione in corso + Modifica + Modifica + Modifica messaggio + 8 ore + 4 settimane + Spenta + 1 giorno + 1 ora + 1 settimana + Scadenza dei messaggi di chat + I messaggi di chat possono scadere dopo un certo periodo di tempo. Nota: I file condivisi in chat non saranno eliminati per il proprietario, ma non saranno più condivisi nella conversazione. + Recupero delle impostazioni di segnalazione non riuscito + Accetta + Rifiuta + Nessun invito in attesa + Indietro + Utente che segue un collegamento pubblico + Tu: %1$s + Inoltra + Inoltra a … + Galleria + Non hai ancora un server?\nFai clic qui per ottenerne uno da un fornitore + Ottieni codice sorgente + Gruppo + Ospite + Accesso ospiti + Consenti ospiti + Digita una password + Imposta una password per limitare chi può utilizzare il collegamento pubblico. + Protezione password + Rispedisci inviti + Condividi collegamento della conversazione + Digita un messaggio … + Conversazione importante + Lo stato utente “Non disturbare” viene ignorato per le conversazioni importanti. + Inviti + Entra nelle conversazioni aperte + Devi promuovere un nuovo moderatore prima di poter lasciare la conversazione. + %1$s | Ultima modifica: %2$s + Lascia la conversazione + Chiusura della chiamata in corso … + GNU General Public License, versione 3 + Licenza + Il limite di %s caratteri è stato raggiunto + Ingresso + Questa riunione è programmata per %1$s + La riunione inizierà presto + Stai attualmente aspettando nell\'ingresso. + La tua posizione attuale + Autorizzazione di geolocalizzazione richiesta + Posizione sconosciuta + Bloccato + Tocca per sbloccare + Non impostato + Segna come letto + Segna come non letto + Non riuscito + Invio del messaggio non riuscito: + Offline + Annulla risposta + Messaggio letto + Messaggio inviato + Moderatore + Nuova conversazione + Visibilità + Menzioni non lette + Messaggi non letti + %1$s non disponibile (non installato o limitato dall\'amministratore) + Ospite + No + Nessun proxy + Non ora + %1$s sul canale di notifica %2$s + Chiamate + Notifica sulle chiamate in arrivo + Messaggi + Notifica sui messaggi in arrivo + Caricamenti + Impostazioni di notifica + Notifica sempre + Notifica su menzione + Non notificare mai + Attualmente non in linea, controlla la tua connettività + OK + Apri la conversazione agli utenti registrati + Apri anche agli utenti dell\'applicazione ospite + Proprietario + Partecipanti + Aggiungi partecipanti + Password + Apri impostazioni + Account non trovato + Chat tramite %s + Spegni microfono + Accendi microfono + Messaggi + Riservatezza + Informazioni personali + Promuovi a moderatore + Conversazione pubblica + Notifiche push disabilitate + Premi per parlare + Con il microfono disabilitato, fai clic e mantieni per utilizzare Premi per parlare + Ricordamelo più tardi + Rimuovi dai preferiti + Rimuovi gruppo e membri + Rimuovi partecipante + Rinomina conversazione + Rinomina + Rispondi + Rispondi in privato + Salva + Salvato correttamente + 30 secondi + 5 minuti + 1 minuto + 10 minuti + 600 + 60 + 30 + 300 + Cerca + Svuota ricerca + Seleziona account + Conversazione delicata + L\'anteprima dei messaggi sarà disabilitata nell\'elenco delle conversazioni e nelle notifiche. + %1$s ha inviato una GIF. + Hai inviato una GIF. + %1$s ha inviato un video. + Hai inviato un video. + %1$s ha inviato un audio. + Hai inviato un audio. + %1$s ha inviato un\'immagine. + Hai inviato un\'immagine. + Prova di connessione al server + Aggiorna il tuo database %1$s + Importazione dell\'account selezionato non riuscita + Il collegamento alla tua interfaccia web di %1$s quando la apri nel browser. + Importa account dall\'applicazione %1$s + Importa account + Importa account dall\'applicazione %1$s + Importa account + Esci dalla manutenzione di %1$s + Termina l\'installazione di %1$s + Prova di connessione + Sul server non è installata un\'applicazione Talk supportata + Indirizzo server https://… + %1$s funziona solo con %2$s 13 e successivi + Imposta una nuova password + Impostazioni + Il tuo account preesistente è stato aggiornato, invece di aggiungerne un nuovo + Avanzate + Aspetto + Chiamate + Ordina alla tastiera di disattivare l\'apprendimento personalizzato (senza garanzie) + Tastiera incognito + Nessun suono + L\'applicazione Talk non è installata sul server sul quale hai provato ad autenticarti + Notifiche + Messaggi + Verifica i contatti in base al numero di telefono per integrare il collegamento di Talk nell\'applicazione dei contatti di sistema + Puoi impostare il tuo numero di telefono in modo che gli altri utenti ti trovino + Digita numero di telefono + Numero di telefono non valido + Numero di telefono impostato correttamente + Numero di telefono + Integrazione numero di telefono + Riservatezza + Host proxy + Password proxy + Porta proxy + Tipo proxy + Nome utente proxy + Condividi il mio stato di lettura e mostra lo stato di lettura degli altri + Stato di lettura + Autorizza nuovamente l\'account + Rimuovi + Rimuovi account + Conferma la tua intenzione di rimuovere l\'account attuale. + Blocca %1$s con il blocco schermo di Android o con il metodo biometrico supportato + Tempo di inattività per il blocco schermo + Blocco schermo + Impedisce le schermate nell\'elenco dei recenti e all\'interno dell\'applicazione + Sicurezza schermo + La versione del server è molto datata e non sarà più supportata nella prossima versione! + La versione del server è troppo datata e non supportata da questa versione dell\'applicazione Android + Server non supportato + Scuro + Usa valori predefiniti di sistema + tema + Chiaro + Tema + Condividi il mio stato di digitazione e mostra lo stato di digitazione degli altri. + Il proxy richiede credenziali + Avviso + Può essere autorizzato nuovamente solo l\'account attuale + Condividi il contatto + Necessaria l\'autorizzazione per leggere i contatti + Condividi la posizione attuale + Collegamento di condivisione + Condividi posizione + Condividi questa posizione + Scegli account + Oggetti condivisi + Scheda di Deck + Nessun elemento condiviso + Posizione + Posizione condivisa + Quando le notifiche non sono impostate correttamente, mostra un avviso regolare + Mostra avviso di notifica regolare + Ordina per + Ora di inizio + Cambia account + Team + Scegli i file + Inviare questi file a %1$s? + Inviare questo file a %1$s? + Spiacenti, caricamento non riuscito + Problema + Condividi da %1$s + Carica dal dispositivo + Caricamento + Scatta foto + Utente + Registrazione Talk da %1$s (%2$s) + Tieni premuto per registrare, rilascia per inviare. + Autorizzazione di registrazione audio richiesta + « Scorri per annullare + Webinar + + Settimana successiva + Nessuna integrazione del numero di telefono a causa di autorizzazioni mancanti + Spento + Predefinito + 1 ora + In linea + Stato in linea + Apri conversazioni + Apri nell\'applicazione File + Riproduci/ferma messaggio vocale + Aggiungi opzione + Opzioni + Sondaggio privato + Domanda + Risultati + Impostazioni + Votare + Impostato in precedenza + Alza la mano + Tutti + La condivisione dei file dall\'archiviazione non è possibile senza permessi + Il permesso di registrazione è richiesto per tutte le call + Richiedi il consenso alla registrazione prima di partecipare alla chiamata in questa conversazione + Permesso di registrazione + La chiamata potrebbe essere registrata. + Registrazione + Ripristina stato + Salva + Scan QR Code + Sincronizza solo con server fidati + Federato + Visibile solo alle persone in questa istanza e agli ospiti + Locale + Visibile solo alle persone trovate con l\'integrazione del numero di telefono via Talk su mobile + Privato + Sincronizza con server fidati e la rubrica globale e pubblica + Pubblicato + Cambio di ambito + Cambia livello di privacy di %1$s + Scorri in fondo + secondi fa + Selezionato + Invia email + Invia a + Invia a… + Imposta + Imposta stato + Imposta messaggio di stato + Condividi + Audio + File + Media + Altro + Sondaggio + Voce + Preferito + Non ti è consentito avviare una chiamata + Messaggio di stato + Scatta una foto + Errore acquisizione immagine + Non è possibile scattare una foto senza autorizzazioni + Ri-scatta foto + Invia + Cambia fotocamera + Ritaglia foto + Riduci dimensione immagine + Accendi/spegni la torcia + 30 minuti + Questa settimana + Questo fine settimana + Oggi + Domani + Traduci + Rileva lingua + Impostazioni dei dispositivi + Impossibile rilevare la lingua + Traduzione fallita + Da + A + Disarchivia conversazione + Non archiviato %1$s + Rimuovi ban + Da leggere + Carica nuovo avatar dal dispositivo + Avatar dell\'utente + Indirizzo + Nome completo + Posta elettronica + Numero di telefono + Twitter + Sito web + Stato + Impossibile ottenere le informazioni personali dell\'utente. + Nessuna informazione personale impostata + Aggiungi nome, immagine e dettagli di contatto sulla tua pagina di profilo. + Qual è il tuo stato? + diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml new file mode 100644 index 0000000..c639afd --- /dev/null +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -0,0 +1,612 @@ + + + 編集 + 追加 + メモに追加する + %1$s会話をお気に入りに追加した + %sを検索 + オフライン + 会話をアーカイブ + アーカイブ済み + Bluetooth + 音声の出力 + 携帯 + スピーカー + 有線ヘッドセット + あなたのステータスは自動的に設定されました + アバター + 離席中 + ビジー + カレンダー + 高度なコールオプション + 通話が1 時間経過 + 通知なしで通話 + カメラの権限が与えられました。もう一度カメラを選択してください。 + ログインをキャンセル + クラウドからアバターを選択 + ステータスメッセージを消去 + ステータスメッセージの有効期限 + 閉じる + 接続が確立しました + 音声メッセージを連続録音するための録音ロック + 会話は読み取り専用です + 会話 + 会話を作成 + 問題を作成する + カスタム + 危険区域 + アバターを削除 + 会話%1$sは削除済みです + 取り込み中 + 消去しない + 編集 + メッセージを編集 + 最近 + 暗号化済み + 通話終了 + 全員の通話を終了する + あなたのチャットの読み込み中に問題が発生しました + %1$sの保存に失敗しました + 15分 + フォルダー + 読み込み中… + %1$s (%2$d) + 4時間 + 保留中の招待を取得することができませんでした + (編集済み) + オフライン + 言語を取得できませんでした + 取得に失敗しました + 今日この後 + 通話終了 + あなたは会話%1$sから離れました + 結果をさらに読み込む + 会話をロック + 鍵の記号 + 手を下げる (r) + 会話%1$sを既読としてマークしました + 会話%1$sを未読としてマークしました + メンションされました + 新しい順 + 日付(昇順) + A - Z + Z - A + サイズ(降順) + サイズ(昇順) + あなたが削除したメッセージ + %1$sに編集されました + タップして投票を開く + 検索結果なし + 入力して検索を開始 … + 検索… + メッセージ + 全ての通知をミュートします + 選択したアカウントがインポートされ、使用可能になりました + バージョン情報 + アクティブなユーザー + アカウントを追加 + アカウントは削除予定であり、変更することはできません + メインメニューを開く + ファイルを添付 + 絵文字追加 + 会話に追加 + 参加を追加 + お気に入りに追加 + OK、すべて完了! + ピン: %1$s + %1$sを解除 + Bluetooth スピーカーを有効にするには、「近くのデバイス」の権限を付与してください。 + ビデオ通話で対応する + 音声通話のみで応答する + 音声出力を変更する + カメラを切り替える + 通話終了 + マイクロフォンを切り替える + ピクチャーインピクチャーモードを開く + セルフビデオに切り替える + 呼び出し中 + 会話名 + 通話通知 + %1$sさんが手を挙げました + 再接続中 … + 呼び出し中 + %1$s着信 + %1$sと電話 + %1$sと映像で + 45秒以内に応答がありませんでした。タップして再試行してください + %s通話 + %s映像通話 + %s音声通話 + ビデオ通信コミュニケーションを有効にするには、「カメラ」の権限を付与してください。 + キャンセル + 機能を取得できませんでした。 + カプション + 未確認のSSL証明書を信頼しますか?( issued by %1$s for %2$s, valid from %3$s to %4$s) + 証明書をチェックアウトする + SSL設定で接続が妨げられました + 認証証明書を変更する + パスワードを変更 + 編集をキャンセル + 編集をキャンセル + すべてのメッセージを削除 + すべてのメッセージが削除されたました。 + 本当にこの会話の全てのメッセージを削除してもよろしいですか? + クライアント証明書を変更する + クライアント証明書を設定する + かつ + コピー + クリップボードにコピーされました + 作成 + 無効 + 閉じる + 不明なエラーが発生しました。 + 追加のオプション + 設定 + スキップ + 不明 + 認証証明書を選択 + 接続中 … + 完了 + 会話概要 + 会話情報 + ビデオ通話 + 音声通話 + 会話が見つかりません + 会話設定 + 会話に参加するか、新しいのを開始 + 友達や同僚に挨拶しましょう! + コピー + 新しい会話を作成する + 投票を作成 + 今日 + 昨日 + 削除 + 全て削除 + 会話を削除 + 会話を削除すると、ほかの参加者でも一緒に解除されます。 + メッセージを削除 + メッセージは削除されましたが、他サービスへは転送されている可能性があります。 + ユーザー%1$sは削除されました + モデレータから降格 + ボイスメッセージを録音 + メッセージを送信 + 現在のアカウント + サーバー + 通知サーバーアプリはインストールされていますか? + ユーザー + ユーザーステータスを有効化しますか? + Androidバージョン + アプリ + アプリ名 + 登録ユーザー + アプリバージョン + バッテリーの最適化は無視されますが、全ては問題ありません + バッテリー設定 + 端末 + トラブルシューティングのチェックリストを開く + 診断画面を開く + dontkillmyapp.comを開く + 最新の Firebase プッシュ トークンを取得する + 最新の Firebase プッシュ トークンの生成 + Firebase プッシュ トークンが設定されていません。不具合の報告を作成してください。 + Firebase プッシュトークン + Google Play サービスは利用できません。通知はサポートされていません + Google Play サービス + Google Playサービスが利用可能 + プッシュプロキシでの最新のプッシュ登録 + プッシュプロキシにまだ登録されていません + サーバーでの最新のプッシュ登録 + サーバーにまだ登録されていません + メタ情報 + システム報告の生成 + 通知チャネルを有効にしますか? + メッセージ通知チャネルを有効にしますか? + 通知権限 + 携帯 + サーバーTalkバージョン + サーバーバージョン + 外部 + 非公開 + シグナリングモード + 無効なパスワード + サーバーは現在メンテナンスモードです。 + アプリが古いです + アプリは古すぎるため、このサーバーではサポートされていません。更新してください。 + 更新 + このアカウントを再認証しますか、それとも削除しますか? + このメディアをストレージに保存すると、デバイス上の他のアプリからアクセスできるようになります。 + 続けますか? + いいえ + ストレージに保存しますか? + はい + 表示名を取得できませんでした。 + 表示名を保存できませんでした。 + 編集 + 編集 + メッセージを編集 + 管理者に編集されました + 8時間 + 4週間 + オフ + 1日 + 1時間 + 1週間 + チャットメッセージの有効期間 + チャットメッセージは、一定時間経過後に期限切れにすることができます。注意: チャットで共有されたファイルは、ファイル所有者は削除されませんが、会話では共有されなくなります。 + シグナリング設定を取得できませんでした + 承諾 + 拒否 + %1$sから%2$s変換 + 保留中の招待はありません + 保留中の招待があります + 戻る + ファイルアクセスの権限が必要です + 公開リンクをたどっているユーザー + あなた:%1$s + 転送 + 次に転送 … + ギャラリー + まだサーバーがありませんか?\ nプロバイダーから取得するにはここをクリックしてください + ソースコードを入手 + グループ + ゲスト + ゲスト参加 + ゲストアクセスの有効化/無効化ができません。 + ゲストがこの会話に参加するための公開的なリンクを共有できるようにします。 + ゲストを許可 + パスワードを入力 + ゲストアクセスパスワード + パスワードの設定/無効化中にエラーが発生しました。 + パスワードを設定して、パブリックリンクを使用できるユーザーを制限します。 + パスワード保護 + 招待を再送 + エラーのため招待状は送信されませんでした。 + 招待は再送信されました + 会話のリンクを共有 + メッセージを入力… + バッテリーの最適化を無視する + 重要な会議 + 招待 + オープンな会話に参加する + 会話から離れる前に、新しいモデレーターを昇格させる必要があります + %1$s最終更新:%2$s + 会話を離れる + 通話を終了 … + GNU General Public License, version 3 + ライセンス + %s文字の制限に達しました + ロビー + この会議は%1$sに予定されています + 会議はまもなく始まります + 現在ロビーで待機中 + あなたの現在地 + 位置情報の許可が必要です + 位置が不明 + ロック + タップしてロック解除 + 未設定 + 既読にする + 未読にする + 失敗しました + メッセージの送信に失敗しました: + 返信をキャンセル + メッセージ既読 + メッセージ送信済 + 音声通信コミュニケーションを有効にするには、「マイクロフォン」の権限を付与してください。 + %sから不在着信 + モデレータ + 新しい会話 + 可視性 + 未読の返信 + 未読のメッセージ + %1$s利用できません(インストールされていないか、管理者によって制限されています) + ゲスト + いいえ + オープンな会話はありません + プロキシを利用しない + オーディオを有効にすることはできません。 + ビデオを有効にすることはできません。 + あとで + %2$s通知チャンネルの%1$s + 呼び出し + 着信について通知 + メッセージ + 受信メッセージについて通知 + アップロード + アップロード進捗について通知する + 通知設定 + 通知のトラブルシューティング + 常に通知する + 言及したときに通知する + 決して通知しない + 現在オフラインです。接続を確認してください。 + OK + 登録ユーザーに会話を公開 + ゲストアプリユーザーにも開放 + オーナー + 参加者 + 参加を追加 + パスワード + 権限セット + 一部の権限が拒否されました。 + 権限を許可してください + 設定を開く + 設定 > 権限で権限を付与してください + アカウントが見つかりません + %sとチャット + マイクをミュート + マイクを有効にする + メッセージ + プライバシー + パーソナル情報 + モデレータに昇格 + 公開会話 + プッシュ通知が無効になっています + プッシュトゥートーク + マイクを無効にして、プッシュツートークを使用する場合は & ホールド + 後で通知する + お気に入りから削除 + メンバーとグループを削除 + 参加者を削除 + チーム・グループとメンバーを削除 + 会話の名前を変更する + 名前を変更 + 返信 + 個人的に返信する + 保存 + 保存しました + 30 秒 + 5分 + 1 分 + 10分 + 600 + 60 + 30 + 300 + 検索 + 検索をクリア + アカウントを選択 + %1$sがGIFを送信しました。 + GIFを送信しました。 + %1$sが動画ファイルを送信しました。 + 動画ファイルを送信しました。 + %1$sが音声ファイルを送信しました。 + 音声ファイルを送信しました + %1$sがイメージファイルを送信しました。 + イメージファイルを送信しました。 + サーバー接続テスト + %1$sデータベースをアップグレードしてください + 選択したアカウントのインポートに失敗しました + %1$sをWeb画面でブラウザーで開くときのURL + %1$sアプリからアカウントをインポートする + アカウントをインポート + %1$sアプリからアカウントをインポートする + アカウントをインポート + %1$sをメンテナンスから外してください + %1$sのインストールを完了してください + 接続テスト + サーバーにサポートされているトークアプリがインストールされていません + サーバーのアドレス https://… + %1$sは%2$s 13以降でのみ動作します + 新しいパスワードの設定 + 設定 + 新しいアカウントを追加する代わりに、既存のアカウントが更新されました + 高度な + 表示 + 呼び出し + 診断画面を開く設定を確認したり、不具合の報告を作成したりします + 診断 + カスタマイズされた入力が認識されないようにする(保証なし) + シークレットモードキーボード + 音無し + 許可を得ようとしたサーバーにトークアプリがインストールされていません + 通知 + 通知が拒否されました + 通知が許可されました + メッセージ + 電話番号に基づいて連絡先を照合し、Talkのショートカットをシステムの連絡先アプリに統合します + エラー429 リクエストが多すぎます + 電話番号を設定すると他のユーザーがあなたを見つけられるようになります + 電話番号を入力 + 電話番号が正しくありません + 電話番号を設定しました + 電話番号 + 電話番号を統合 + プライバシー + プロキシホスト + プロキシのパスワード + プロキシポート + プロキシタイプ + プロキシのユーザー名 + 自分の読み取りステータスを共有し、他の人の読み取り状態を表示します + 読み取りステータス + アカウントの再認証 + 削除 + アカウントを削除 + 現在のアカウントを削除する目的を確認してください。 + %1$sをアンドロイドスクリーンロックかサポートされている生体認証でロックします。 + タイムアウト時のスクリーンロック + スクリーンロック + 最近のリストとアプリ内でのスクリーンショットを防止します。 + スクリーンセキュリティ + サーバーバージョンが古すぎるため、次のリリースのバージョンではサポートされません。 + サーバーのバージョンが古く、このAndroidバージョンで対応していません。 + サポートされてないサーバーです + ダーク + デフォルトの設定を使用する + テーマ + ライト + テーマ + 自分のタイピング状況を共有し、他の人のタイピング状況を表示する + 入力ステータスは、高性能パフォーマンスのバックエンド (HPB) を使用している場合にのみ利用できます。 + 入力ステータス + プロキシは資格情報を必要とします + 警告 + 現在のアカウントのみ再認可できます + 連絡先を共有 + 連絡先を読み取る権限が必要です + 現在の位置を共有 + 共有リンク + 位置を共有 + この位置を共有 + アカウントを選択 + 共有済みアイテム + Deckカード + 画像、ファイル、ボイスメッセージ … + 共有アイテムがありません + 位置 + 共有された位置情報 + ソート: + 開始時刻 + アカウントの切り替え + チーム + ファイルを選択 + %1$sへこれらのファイルを送信しますか? + %1$sへこのファイルを送信しますか? + アップロードできませんでした + %1$sアップロードに失敗しました + 失敗 + %1$sから共有 + デバイスからアップロード + アップロード中 + %1$sから%2$s-%3$s\%% + 写真を撮る + ビデオを撮る + ユーザー + %1$sからビデオレコーディング + %1$s(%2$s)からの会話録音 + 長押しして録音、離して送信 + 音声の録音を許可する権限が必要です + « スライドしてキャンセル + ウェビナー + はい + 来週 + 利用権限がないため、電話番号統合ができません。 + \@で直接会話 + デフォルト + 1時間 + オンライン + オンラインステータス + オープンな会話 + アプリでファイルを開く + 音声メッセージを再生/一時停止 + オプションを追加 + 投票を修正する + 投票を終了する + 本当にこのアンケートを終了しますか? これは元に戻すことができません + このアンケートでは、これ以上のオプションで投票することはできません。 + 複数の答え + オプション + プライベート投票 + 質問 + 質問 + 結果 + 設定 + 投票 + 投票を送信しました + 以前の設定 + 挙手 (r) + すべて + ストレージからのファイル共有は権限がなければ不可能です + 音声通話のみで応答する + 録画開始をキャンセル + レコーディングが失敗しました。あなたの管理者に連絡してください。 + 録画を開始 + あなたは本当に録音を停止しますか? + 通話録音を停止 + 録画を停止 + レコーディングを停止しています + 全ての通話には録音の同意が必要です。 + この会話に参加する前に、録音の同意が必要です。 + レコーディングの同意 + 通話は録音されるかもしれません。 + 記録中 + 会話%1$sをお気に入りから削除済みです + %1$s会話が名前変更されました + ステータスをリセット + 通話中に他の部屋に参加することはできません + 保存 + Scan QR Code + 信頼できるサーバーのみと同期 + 連携 + このインスタンスのユーザーとゲストにのみ表示 + ローカル + モバイルのTalkで電話番号が一致した人にのみ表示されます + 非公開 + 信頼できるサーバーへ、グローバルおよびパブリックアドレスブックを同期 + 公開されました + 公開範囲を切替 + プライバシーレベルを%1$sに変更 + 下にスクロール + 秒前 + 選択済み + メールを送信 + 送信 + このチャットにコンテンツを許可する権限がありません + 送信… + 通知なしで送信 + セット + カメラからアバターを設定する + ステータスを設定 + メッセージを設定 + 共有 + %2$sで会話%1$sに参加する + オーディオ + ファイル + メディア + その他 + 投票 + レコーディング + 音声番号 + お気に入り + 通話を開始することが許可されていません + 会話を開始 + ステータスメッセージ + ブレイクアウトルームに切り替える + メインルームに切り替える + 写真を撮る + 写真の撮影エラー + 写真の撮影は権限なしには行えません + 再撮影 + 送信 + カメラ切替 + 写真をクロップ + 画像サイズを削減 + フラッシュを切替 + 30分 + 今週 + これはテストメッセージです + この週末 + 今日 + 明日 + 翻訳 + 翻訳 + 翻訳されたテキストをコピー + 言語を検出する + デバイスの設定 + 言語を検出できませんでした + 翻訳に失敗しました + 差出人 + 宛先 + 他の 1 人が入力中です… + 入力中... + 入力中... + 他の%1$s人が入力中です… + 未読 + デバイスから新しいアバターをアップロード + ユーザーのアバター + 住所 + フルネーム + メールアドレス + 電話番号 + Twitter + ウェブサイト + ステータス + ユーザー情報を取得できませんでした + 個人情報はありません + プロフィールページに名前、写真、連絡先の詳細を追加します。 + 現在のオンラインステータスは? + + %d投票数 + + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000..2b115ba --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,508 @@ + + + 편집 + 추가 + %s 검색 + 접속 안함으로 표시 + 보관된 + 블루투스 + 오디오 출력 + 전화 번호 + 스피커 + 유선 헤드셋 + 아바타 + 자리비움 + 바쁨 + 일정 + 통화가 한 시간동안 진행되었습니다. + 알림 없이 전화하기 + 카메라 권한이 부여되었습니다 . 카메라를 다시 선택해주세요. + 로그인 취소 + 클라우드에서 아바타 선택 + 상태 메시지 지움 + 상태 메시지 지우기 예약 + 닫기 + 연결됨 + 대화 + 대화 만들기 + 사용자 정의 + 위험 지역 + 아바타 삭제 + 방해 없음 + 지우지 않음 + 편집 + 메시지 편집 + 최근 항목 + 암호화 + 모든 이들에 대해 통화 끝내기 + 당신의 채팅을 불러들이는데 문제가 생겼습니다. + %1$s 저장 실패 + 15분 + 폴더 + 불러오는 중 … + %1$s (%2$d) + 4 시간 + (수정됨) + 숨겨짐 + 오늘 중 + 통화 떠나기 + 더 많은 결과 불러오기 + 대화 잠금 + 자물쇠 기호 + 손 내리기 + 최신순 + 오래된순 + A - Z + Z - A + 큰 것 먼저 + 작은 것 먼저 + 메시지를 삭제함 + 투표를 열기 위하여 탭하세요. + 결과를 찾을 수 없음 + ...를 검색하기 위하여 입력을 시작합니다 + ...를 검색 + 메시지 + 모든 알림을 음소거 + 선택한 계정을 가져왔고 사용할 수 있음 + 정보 + 활성 사용자 + 계정 추가 + 이 계정이 삭제 예정이며 변경할 수 없음 + 메인메뉴 열기 + 첨부파일 추가 + 이모티콘 추가 + 대화에 추가 + 참가자 추가 + 즐겨찾기에 추가 + 모두 완료되었습니다! + 핀: %1$s + %1$s 잠금 해제 + 영상 통화로 응답합니다. + 영상 통화로만 응답합니다. + 오디오 출력을 바꾸다 + 카메라 전환 + 끊기 + 마이크 전환 + 셀프 영상으로 전환 + 대화명 + 전화 알림 + 재연결 ... + 45초 동안 응답이 없다면 다시 시도하기를 눌러주십시오. + %s 통화 + %s 영상 통화 + %s 음성 통화 + 취소 + 기능을 가져올 수 없음, 중단함 + %1$s에서 %2$s에게 발급한 %3$s부터 %4$s까지 유효한 알 수 없는 인증서를 신뢰하시겠습니까? + 인증서 확인 + SSL 설정에서 연결을 거부함 + 인증서 변경 + 암호 변경 + 수정 취소 + 수정 취소 + 모든 메시지 삭제 + 모든 메시지가 삭제되었습니다. + 이 대화에서 모든 메시지를 삭제하시겠습니까? + 클라이언트 인증서 병경 + 클라이언트 인증서 설정 + 그리고 + 복사 + 클립보드로 복사됨 + 생성 + 비활성화됨 + 무시 + 죄송합니다, 무언가 잘못되었습니다! + 설정 + 건너뛰기 + 알 수 없음 + 인증서 선택 + 연결 중 … + 완료 + 대화 정보 + 영상 통화 + 음성 통화 + 대화 설정 + 대화에 참가하거나 새 대화 시작 + 여러분의 친구들과 동료들에게 인사하세요! + 복사 + 새 대화 만들기 + 투표 만들기 + 오늘 + 어제 + 삭제 + 모두 삭제 + 대화 삭제 + 대화를 삭제하면 다른 모든 참가자에게서도 삭제됩니다. + 메시지 삭제 + 메시지가 성공적으로 삭제되었지만, 다른 서비스에 노출되었을 수 있습니다. + 중재자 권한 제거 + 음성 메시지 녹음 + 메시지 보내기 + 현재 계정 + 서버 + 사용자 + 안드로이드 버전 + + 앱 이름 + 등록된 사용자 + 배터리 설정 + 장치 + 외부 + 내부 + 잘못된 암호 + 업데이트 + 이 계정을 재인증하거나 삭제하시겠습니까? + 아니오 + + 표시 이름을 가져올 수 없음, 중단함 + 표시 이름을 저장할 수 없음, 중단함 + 편집 + 편집 + 메시지 편집 + 8시간 + 4주 + 꺼짐 + 1일 + 1시간 + 1주 + 채팅 메시지 만료 + 채팅 메시지가 특정 시간 이후에 만료될 수 있습니다. 참고: 채팅에서 공유된 파일들은 소유자에게서 삭제 되지 않지만, 대화에서 더이상 공유되지 않습니다. + 신호 설정을 가져오지 못함 + 수락 + 거부 + 뒤로 + 공개 링크를 따르는 사용자 + 나: %1$s + 전달 + ...에 전달 + 갤러리 + 서버가 없으신가요?\n공급자를 알아보려면 누르십시오 + 소스 코드 얻기 + 그룹 + 손님 + 게스트 접속 + 손님 접근을 가능/불가능 하게 할 수 없습니다. + 이 대화에 참가할 수 있는 공개 링크를 손님이 공유할 수 있도록 허용합니다. + 손님 허용 + 암호 입력 + 손님 액세스 암호 + 암호 설정/비활성화 중 오류 발생 + 공개 링크를 사용할 수 있는 사용자를 제한하려면 암호를 설정하십시오. + 암호 보호 + 초대 재전송 + 초대장이 오류로 인하여 전송되지 못했습니다. + 초대장이 다시 전송되었습니다. + 대화 링크 공유 + 메시지 입력… + 중요한 대화 + 공개 대화에 참가 + 보관 + 대화를 떠나기 전 새 중재자를 임명해야 합니다. + %1$s | 최종 수정: %2$s + 대화 나가기 + 통화 종료 중 ... + GNU General Public License, Version 3 + 라이선스 + %s 문자 제한에 도달됨 + 로비 + 이 미팅은 %1$s로 예정되어 있습니다. + 회의가 곧 시작됩니다. + 현재 로비에서 대기중 + 당신의 현재 위치 + 위치 사용 권한이 필요합니다. + 위치를 알 수 없음 + 잠긴 + 탭하여 잠금해제 + 설정되지 않음 + 읽은 것으로 표시 + 읽지 않은 것으로 표시 + 실패 + 메시지 전송을 실패하였습니다: + 응답 취소 + 메시지 읽음 + 보낸 메시지 + %s로부터의 전화를 받지 못하였습니다. + 중재자 + 새 대화 + 표시 여부 + 읽지 않은 언급 + 읽지 않은 메시지 + %1$s을(를) 사용할 수 없음 (설치되지 않았거나 관리자에 의해 제한됨) + 손님 + 아니요 + 프록시 없음 + 오디오를 활성화 할 수 없습니다! + 영상을 활성화할 수 없습니다! + 지금은 하지 않음 + %2$s 알림 채널의 %1$s + 통화 + 수신 통화 알림 + 메시지 + 수신 메시지 알림 + 업로드 + 업로드 진행에 알림 + 알림 설정 + 항상 알림 + 언급 시 알림 + 알림 끄기 + 현재 오프라인 상태입니다. 연결 상태를 확인하십시오. + 완료 + 등록된 사용자에게 대화 열기 + 손님인 앱 사용자에게도 개방 + Owner + 참가자 + 참가자 추가 + 암호 + 설정 열기 + 계정을 찾을 수 없습니다. + %s을 통한 채팅 + 음소거 마이크 + 마이크 활성화 + 메시지 + 개인 정보 취급 방침 + 개인 정보 + 중재자 권한 부여 + 푸시 알림이 비활성화됨 + 눌러 말하기 + 마이크가 비활성화된 상태에서 &를 누르고 있으면 눌러 말하기 사용 + 나중에 다시 알림 + 즐겨찾기에서 제거 + Remove group and members + 참가자 삭제 + 대화 제목 변경 + 이름 바꾸기 + 답장 + 비공개로 답장 + 저장 + 30 초 + 5분 + 1분 + 10분 + 600 + 60 + 30 + 300 + 검색 + 찾기 초기화 + 계정 선택 + %1$s님이 GIF를 보냈습니다. + GIF를 보냈습니다. + %1$s님이 영상 파일을 보냈습니다. + 영상 파일을 보냈습니다. + %1$s님이 오디오 파일을 보냈습니다. + 오디오 파일을 보냈습니다. + %1$s님이 이미지를 보냈습니다. + 이미지를 보냈습니다. + 서버 연결 테스트 + %1$s 데이터베이스를 업그레이드하십시오 + 선택한 계정을 가져올 수 없음 + %1$s 웹 인터페이스를 브라우저에서 열면 링크됩니다. + %1$s 앱에서 계정 가져오기 + 계정 가져오기 + %1$s 앱에서 계정 가져오기 + 계정 가져오기 + %1$s의 관리 모드를 종료하십시오 + %1$s 설치를 완료하십시오 + 연결 테스트 중 + 서버에 지원되는 토크 앱이 설치되어 있지 않습니다. + 서버 주소 https://… + %1$s은(는) %2$s 13 이상에서만 작동합니다 + 새 암호 설정 + 설정 + 새 계정을 추가하는 대신 기존 계정이 업데이트됨 + 고급 + 외형 + 통화 + 키보드에 개인화된 학습을 비활성화하도록 지시(보증 없음) + 시크릿 키보드 + 무음 + 인증하려는 서버에 토크 앱이 설치되어 있지 않습니다. + 알림 + 메시지 + 전화번호를 설정하여 다른 사용자가 당신을 찾도록 할 수 있습니다. + 전화번호를 입력 + 유효하지 않은 전화 번호 + 전화번호가 설정됨 + 휴대폰 번호 + 전화번호 통합 + 프라이버시 + 프록시 호스트 + 프록시 암호 + 프록시 포트 + 프록시 종류 + 프록시 사용자 이름 + 내 읽기 상태 공유 및 다른 사용자의 읽기 상태 표시 + 읽기 상태 + 계정 재인증 + 제거 + 계정 삭제 + 현재 계정을 삭제하려는 의사를 확인해주십시오. + Android 화면 잠금 또는 지원되는 생체 인식 방법으로 %1$s 잠금 + 화면 잠금 비활성화 시간 초과 + 화면 잠금 + 현재 리스트와 앱 내부에서 스크린샷이 제한됩니다. + 화면 보안 + 서버버전이 매우 오래되었으며 다음 릴리스에서 지원되지 않습니다! + 서버 버전이 매우 오래 되어 이 안드로이드 앱 버전에서 지원되지 않습니다. + 지원되지 않는 서버 + 어두움 + 시스템 기본 설정 사용 + 테마 + 밝음 + 테마 + 내 입력 상태를 공개하고 다른 이들의 입력 상태 보기 + 프록시가 인증 정보를 요구합니다. + 경고 + 현재 계정만 다시 인증할 수 있음 + 연락처 공유 + 주소록을 읽기 위하여 권한이 필요합니다. + 현재 위치 공유 + 위치 공유 + 이 위치를 공유 + 계정 선택 + 공유된 항목들 + Deck 카드 + 이미지, 파일, 음성 메시지 ... + 공유된 항목이 없습니다. + 위치 + 공유된 위치 + 정렬 + 시작 시간 + 계정 전환 + + 파일 선택 + %1$s에게 이 파일들을 전송하시겠습니까? + %1$s에게 이 파일을 전송하시겠습니까? + 죄송합니다, 업로드에 실패하였습니다. + %1$s를 업로드 하지 못하였습니다. + 실패 + %1$s로부터 공유 + 이 장치에서 업로드하다 + 업로드 중 + 사진 찍기 + 영상 촬영 + 사용자 + %1$s(으)로 부터의 영상 녹화 + %1$s(%2$s) 부터의 음성녹음 + 오디오 녹음을 위하여 권한이 필요합니다. + « 취소하려면 슬라이드하세요. + Webinar + + 다음주 + 권한 없음으로 인하여 전화 번호를 통합하지 않습니다. + \@-언급만 + 디폴트 + 1시간 + 접속 중 + 접속 상태 + 대화 열기 + 파일 앱에서 열다 + 음성 메시지를 재생/중단 + 옵션을 추가하다 + 투표를 편집하다 + 투표를 끝내다 + 이 투표를 정말 종료하시겠습니까? 이 실행은 다시 되돌릴 수 없습니다. + 이 투표에 더 많은 옵션으로 투표할 수 없습니다. + 다수의 답변 + 옵션들 + 개인 옵션 + 질문 + 당신의 질문 + 결과들 + 설정 + 투표 + 투표가 제출되었습니다. + 손들기 + 모두 + 허가 없이 저장소에서 파일을 공유할 수 없습니다. + 녹음/녹화 시작 취소 + 녹음/녹화가 실패했습니다. 관리자에게 연락해 주세요. + 녹음/녹화 시작 + 녹음/녹화 중단 + 모든 통화에서 녹음/녹화 동의 필요 + 이 대화에 참여하기 전 녹음/녹화 동의 필요 + 녹음/녹화 동의 + 통화가 녹음/녹화됩니다. + 녹음/녹화 중 + 통화 중 다른 대화방에 접속할 수 없습니다 + 저장 + Scan QR Code + 신뢰할 수 있는 서버만 동기화할 수 있습니다. + 연합 + 이 인스턴스의 사용자와 손님만 볼 수 있음 + 로컬 + 전화번호 통합으로 연결된 사용자에게만 보임 + 비공개 + 신뢰할 수 있는 서버와 전역 및 개인 연락처에 동기화 + 발행됨 + 범위 전환 + %1$s의 프라이버시 수준에 대한 변경 + 아래로 스크롤 + 초 전 + 선택 + 이메일 보내기 + 에게 전송 + 이 채팅에 콘텐츠를 공유할 수 없습니다. + ... 에게 전송 + 알림 없이 전송 + 설정 + 카메라에서 아바타를 설정하다 + 상태 설정 + 상태 메시지 설정 + 공유 + 오디오 + 파일 + 미디어 + 기타 + 투표 + 통화 녹음 + 음성 번호 + 즐겨찾기 + 통화를 시작하도록 허가되지 않았습니다. + 상태 메시지 + 소회의실로 전환 + 주 대화방으로 전환 + 사진 찍기 + 사진 촬영 오류 + 허가 없이 사진을 찍을 수 없습니다. + 사진을 다시 찍다 + 보내기 + 카메라 전환 + 사진 자르기 + 이미지 크기를 줄이다 + 토치 전환 + 30분 + 이번 주 + 이번 주말 + 오늘 + 내일 + 번역: + 번역 + 번역한 텍스트 복사 + 언어 감지 + 디바이스 설정 + 언어를 감지할 수 없음 + 번역 실패 + 보낸사람 + 받는사람 + 및 다른 1 명이 입력 중 ... + 이(가) 입력 중 ... + 및 다른 %1$s명이 입력 중 ... + 읽지 않음 + 장치에서 새로운 아바타를 업로드하다 + 사용자 아바타 + 주소 + 전체 이름 + 이메일 + 휴대폰 번호 + Twitter + 웹 사이트 + 상태 + 개인 사용자 정보를 검색하지 못했습니다. + 개인 정보 설정되지 않음 + 프로필 페이지에 이름, 사진, 연락처를 추가하십시오. + 당신의 상태는? + + %d 표 + + diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 0000000..28a9a9f --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,13 @@ + + + + + 24dp + 110dp + diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml new file mode 100644 index 0000000..36191bd --- /dev/null +++ b/app/src/main/res/values-lt-rLT/strings.xml @@ -0,0 +1,398 @@ + + + Taisyti + Pridėti + Ieškoti %s + Atrodyti atsijungusiu + Archyvuota + Bluetooth + Garso išvestis + Telefonas + Jūsų būsena buvo nustatyta automatiškai + Avataras + Atsitraukęs + Užimtas laikas + Kalendorius + Išplėstinės skambučio parinktys + Atsisakyti prisijungimo + Išvalyti būsenos žinutę + Išvalyti būsenos žinutę po + Užverti + Ryšys užmegztas + Pokalbis yra tik skaitymui + Pokalbiai + Sukurti pokalbį + Tinkinti + Pavojinga zona + Netrukdyti + Neišvalyti + Taisyti + Taisyti laišką + Paskiausi + Nepavyko įrašyti %1$s + 15 minučių + aplankas + Įkeliama… + %1$s (%2$d) + 4 valandos + Nematomas + Šiandien vėliau + Išeiti iš skambučio + Įkelti daugiau rezultatų + Užrakinti pokalbį + Nuleisti ranką + Naujausi pirma + Seniausi pirma + A – Ž + Ž – A + Didžiausi pirma + Mažiausi pirma + Jūs ištrynėte žinutę + Nėra paieškos rezultatų + Rašykite norėdami atlikti paiešką… + Ieškoti… + Žinutės + Išjungti visus pranešimus + Dabar, pasirinkta paskyra yra importuota ir prieinama + Apie + Aktyvus naudotojas + Pridėti paskyrą + Paskyra yra suplanuota ištrynimui ir negali būti pakeista + Atverti pagrindinį meniu + Pridėti priedą + Pridėti šypsenėlių + Pridėti į pokalbį + Pridėti dalyvių + Pridėti į mėgstamus + Gerai, viskas atlikta! + Atrakinti %1$s + Keisti garso išvestį + Perjungti kamerą + Padėti ragelį + Perjungti mikrofoną + Pokalbio pavadinimas + %1$s pakėlė ranką + Jungiamasi iš naujo… + Per 45 sekundes jokio atsakymo, bakstelėkite norėdami bandyti dar kartą + %s skambutis + %s vaizdo skambutis + Atsisakyti + Ar pasitikite iki šiol nežinomu SSL liudijimu, kurį išdavė %1$s, kuris skirtas %2$s ir galioja nuo %3$s iki %4$s? + Jūsų SSL sąranka neleido ryšio + Keisti tapatybės nustatymo liudijimą + Pakeisti slaptažodį + Ištrinti visas žinutes + Visos žinutės buvo ištrintos + Ar tikrai norite ištrinti visas šiame pokalbyje esančias žinutes? + Keisti kliento programėlės liudijimą + Nustatyti kliento programėlės liudijimą + ir + Kopijuoti + Nukopijuota į iškarpinę + Sukurti + Išjungta + Atmesti + Atleiskite, kažkas nutiko! + Nustatyti + Praleisti + Nežinoma + Pasirinkti tapatybės nustatymo liudijimą + Jungiamasi… + Atlikta + Pokalbio aprašas + Pokalbio informacija + Vaizdo skambutis + Balso skambutis + Pokalbis nerastas + Pokalbio nustatymai + Prisijunkite prie pokalbio arba pradėkite naują + Pasisveikinkite su savo draugais ir kolegomis! + Kopijuoti + Sukurti naują pokalbį + Sukurti apklausą + Šiandien + Vakar + Ištrinti + Ištrinti visus + Ištrinti pokalbį + Jei ištrinsite pokalbį, jis taip pat bus ištrintas visiems kitiems dalyviams. + Ištrinti laišką + Pažeminti iš moderatorių + Siųsti žinutę + Dabartinė paskyra + Serveris + Naudotojas + Android versija + Programėlė + Programėlės pavadinimas + Programėlės versija + Akumuliatoriaus nustatymai + Įrenginys + „Google Play“ paslaugos neprieinamos. Pranešimai nepalaikomi + „Google Play“ paslaugos + „Google Play“ paslaugos yra prieinamos + Metainformacija + Telefonas + Serverio versija + Išorinė + Vidinis + Neteisingas slaptažodis + Atnaujinti + Tęsti? + Ne + Taip + Taisyti + Taisyti + Taisyti laišką + 8 valandos + 4 savaitės + Išjungta + 1 diena + 1 valanda + 1 savaitė + Nepavyko gauti signalizavimo nustatymų + Priimti + Atmesti + Atgal + Jūs: %1$s + Persiųsti + Persiunčiama į … + Galerija + Neturite serverio? Spauskite čia, kad galėtumėte pasirinkti vieną iš paslaugų teikėjų + Gauti išeities kodą + Grupė + Svečias + Leisti svečius + Enter a password + Apsauga slaptažodžiu + Siųsti pakvietimus iš naujo + Svarbus pokalbis + Pakvietimai + Prisijungti prie atvirų pokalbių + Palikti + %1$s | Paskutinį kartą modifikuota: %2$s + Išeiti iš pokalbio + GNU Bendroji Viešoji Licencija, Versija 3 + Licencija + Pasiekta %s simbolių riba + Laukimo salė + Šis susitikimas yra suplanuotas %1$s + Susitikimas netrukus prasidės + Šiuo metu laukiate laukimo salėje. + Bakstelėkite, norėdami atrakinti + Nenustatyta + Žymėti kaip skaitytą + Žymėti kaip neskaitytą + Nepavyko + Žinutė išsiųsta + Moderatorius + Naujas pokalbis + Matomumas + Neskaitytos žinutės + Svečias + Ne + Be įgaliotojo serverio + Ne dabar + Skambučiai + Žinutės + Įkėlimai + Pranešimų nustatymai + Visada pranešti + Pranešti, kai mane pamini + Niekada nepranešti + Šiuo metu atsijungta nuo interneto, patikrinkite savo ryšį + Gerai + Savininkas + Dalyviai + Pridėti dalyvius + Slaptažodis + Atverti nustatymus + Paskyra nerasta + Išjungti mikrofoną + Įjungti mikrofoną + Žinutės + Privatumas + Asmeninė informacija + Paaukštinti į moderatorius + Viešas pokalbis + \"Push\" pranešimai yra išjungti + Priminti vėliau + Šalinti iš mėgstamų + Šalinti grupę ir narius + Šalinti dalyvį + Šalinti slaptažodį + Šalinti komandą ir narius + Pervadinti pokalbį + Pervadinti + Atsakyti + Atsakyti privačiai + Įrašyti + Sėkmingai įrašyta + 30 sekundžių + 5 minutės + 1 minutė + 10 minučių + 600 + 60 + 30 + 300 + Ieškoti + Išvalyti paiešką + Pasirinkti paskyrą + %1$s išsiuntė GIF paveikslą. + Jūs išsiuntėte GIF paveikslą. + %1$s išsiuntė vaizdo įrašą. + Jūs išsiuntėte vaizdo įrašą. + %1$s išsiuntė garso įrašą. + Jūs išsiuntėte garso įrašą. + %1$s išsiuntė paveikslą. + Jūs išsiuntėte paveikslą. + Prisijunkite prie serverio + Atnaujinkite savo %1$s duomenų bazę + Nepavyko importuoti pasirinktą paskyrą + Nuoroda į jūsų %1$s saityno sąsają, kuomet atveriate ją naršyklėje. + Importuoti paskyrą iš programėlės %1$s + Importuoti paskyrą + Importuoti paskyras iš programėlės %1$s + Importuoti paskyras + Išjunkite savo %1$s techninės priežiūros veikseną + Užbaikite savo %1$s diegimą + Bandomas sujungimas + Serveryje nėra įdiegta palaikoma Pokalbių programėlė + Serverio adresas https://… + %1$s veikia tik naudojant %2$s 13 ar naujesnę versiją + Nustatyti naują slaptažodį + Nustatyti slaptažodį + Nustatymai + Vietoj naujos paskyros pridėjimo, buvo atnaujinta jūsų esama paskyra + Išplėstiniai + Išvaizda + Skambučiai + Inkognito klaviatūra + Be garso + Pokalbių programėlė nėra įdiegta serveryje, kuriame bandėte nusistatyti tapatybę + Pranešimai + Žinutės + Įveskite telefono numerį + Telefono numeris sėkmingai nustatytas + Telefono numeris + Privatumas + Įgaliotojo serverio prievadas + Įgaliotojo serverio tipas + Šalinti + Šalinti paskyrą + Patvirtinkite savo ketinimą pašalinti esamą paskyrą. + Užrakinti %1$s naudojant Android ekrano užraktą ar palaikomą biometrinį metodą + Ekrano užraktas + Neleidžia daryti ekrano kopijų paskiausiųjų sąraše ir programėlės viduje + Ekrano saugumas + Nepalaikomas serveris + Tamsus + Naudoti sistemos numatytąjį + apipavidalinimas + Šviesus + Apipavidalinimas + Įspėjimas + Iš naujo įgaliota gali būti tik esama paskyra + Viešinio nuoroda + Bendrinti šią vietą + Parinkti paskyrą + Bendrinami elementai + Paveikslai, failai, balso žinutės… + Nėra bendrinamų elementų + Vieta + Rikiuoti pagal + Pradžios laikas + Perjungti paskyrą + Komanda + Nesėkmė + Įkeliama + Fotografuoti + Naudotojas + Laikykite, kad įrašinėtumėte, atleiskite, kad išsiųstumėte. + « Perbraukite norėdami atsisakyti + Internetinis seminaras + Taip + Kita savaitė + Numatytasis + 1 valanda + Prisijungęs + Prisijungimo būsena + Atverti pokalbius + Atkurti/pristabdyti balso žinutę + Pridėti variantą + Parinktys + Privati apklausa + Klausimas + Jūsų klausimas + Rezultatai + Nustatymai + Balsuoti + Pakelti ranką + Visos + Įrašyti + Scan QR Code + Sinchronizuoti tik į patikimus serverius + Federacinis + Matoma tik žmonėms šiame egzemplioriuje ir svečiams + Vietinis + Matoma tik žmonėms, su kuriais pokalbių programėlėje mobiliajame įrenginyje buvo rasta atitiktis per telefono numerio integraciją + Privatus + Sinchronizuoti į patikimus serverius ir į visuotinę bei viešą adresų knygą + Paskelbtas + Slinkti į apačią + prieš keletą sekundžių + Pasirinkta + Siųsti elektroninį laišką + Siųsti be pranešimo + Nustatyti + Nustatyti būseną + Nustatyti būsenos žinutę + Bendrinti + Garso įrašai + Failas + Medija + Kita + Apklausa + Mėgstamas + Būsenos žinutė + Fotografuoti + Klaida fotografuojant + Fotografuoti iš naujo + Siųsti + Perjungti kamerą + Apkirpti nuotrauką + 30 minučių + Šią savaitę + Tai yra bandomoji žinutė + Šį savaitgalį + Atsakyt + Šiandiena + Rytoj + Verskite + Aptikti kalbą + Įrenginio nustatymai + Nepavyko aptikti kalbos + Nuo + Kam + ir dar 1 rašo… + rašo… + rašo… + ir dar %1$s rašo… + Adresas + Vardas, pavardė + El. paštas + Telefono numeris + Twitter + Svetainė + Būsena + Asmeninės informacijos nėra + Pridėkite vardą, nuotrauką ir kontaktinę informaciją savo profilio puslapyje. + Kokia jūsų būsena? + + %d balsas + %d balsai + %d balsų + %d balsas + + diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 0000000..1f756ec --- /dev/null +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,633 @@ + + + Rediger + Legg til + Legg til notater + La til samtale %1$s i favoritter + Søk i %s + Vis som frakoblet + Arkivert + Bluetooth + Lydutgang + Telefon + Høytaler + Kablede hodetelefoner + Statusen din ble satt + Avatar + Borte + Tilbake-knapp + Utesteng + Utesteng deltaker + Liste over utestengelser + Opptatt + Kalender + Avanserte samtalealternativer + Samtalen har pågått i én time. + Ring uten varsel + Kameratillatelse er gitt. Vennligst velg kamera på nytt. + Avbryt pålogging + Velg avatar fra skyen + Fjern statusmelding + Fjern statusmelding etter + Lukk + Lukk ikon + Tilkobling opprettet + Tilkobling brutt – sendte meldinger legges i kø + Lås opptak for kontinuerlig opptak av talemeldingen + Samtalen kan kun leses + Samtaler + Opprett samtale + Opprett problem + Egendefinert + Faresone + Slett data + Slettet samtale %1$s + Ikke forstyrr + Ikke fjern + Rediger + Rediger melding + Nylig + Kryptert + Avslutt samtale + Avslutt samtale for alle + Det oppstod et problem med å laste inn chattene dine + Det oppstod feil ved oppheving av utestengelse av deltaker + Kunne ikke lagre %1$s + 15 minutter + mappe + Laster ... + %1$s (%2$d) + 4 timer + Henting av ventende invitasjoner feilet + (redigert) + Intern merknad + Usynlig + Språk kunne ikke hentes + Henting feilet + Senere i dag + Forlat samtalen + Du forlot samtalen %1$s + Hent flere resultat + Lås samtalen + Lås symbol + Senk hånden + Merket samtale %1$s som lest + Merket samtale %1$s som ulest + Nevnt + Nyeste først + Eldste først + A - Å + Å - A + Største først + Minste først + Melding slettet av deg + Redigert av %1$s + Trykk for å åpne avstemningen + Ingen søkeresultater + Begynn å skrive for å søke... + Søk... + Meldinger + Demp alle varslinger + Valgt konto er nå importert og tilgjengelig + Om + Aktiv bruker + Legg til en konto + Kontoen er planlagt slettet, og kan ikke endres. + Åpne hovedmenyen + Legg til vedlegg + Legg til emojier + Legg til i samtale + Legg til deltager + Legg til favoritter + OK, ferdig! + PIN: %1$s + Lås opp %1$s + For å aktivere Bluetooth-høyttalere, vennligst gi \"Enheter i nærheten\" tillatelse. + Svar som videosamtale + Svar bare som taleanrop + Endre lydutgang + Kamera av/på + Legg på + Mikrofon av/på + Åpne bilde-i-bilde-modus + Bytt til selvvideo + INNKOMMENDE + Samtalenavn + Varsler om anrop + %1$s rekte opp hånden + Kobler til igjen... + RINGER + %1$s i samtale + %1$s med telefon + %1$s med video + Ingen svar på 45 sekunder, trykk for å prøve igjen + %s anrop + %s videosamtale + %s taleanrop + For å aktivere videokommunikasjon, vennligst gi \"Kamera\" tillatelse. + Avbryt + Kunne ikke hente evner, avbryter + Bildetekst + Ønsker du å stole på det hittil ukjente SSL-sertifikatet, utstedt av %1$s for %2$s gyldig fra %3$s til %4$s? + Sjekk sertifikatet + Ditt SSL-oppsett forhindret tilkobling + Endre sertifikat for autentisering + Endre passord + Avbryt redigering + Avbryt redigering + Slett alle meldinger + Alle meldinger ble slettet + Ønsker du virkelig å slette alle meldinger i denne samtalen? + Endre klient sertifikat + Sett opp klient sertifikat + og + Kopi + Kopiert til utklippstavlen + Opprett + Deaktivert + Forkast + Noe gikk galt. + Mer innstillinger + Sett + Hopp over + Ukjent + Velg sertifikat for autentisering + Kobler til... + Ferdig + Samtalebeskrivelse + Samtale informasjon + Videosamtale + Samtale + Samtalen ble ikke funnet + Samtale innstillinger + Ta del i en samtale eller start en ny + Si hei til venner og kolleger! + Kopier + Opprett en ny samtale + Opprett avstemning + I dag + I går + Slett + Slett alle + Slett samtale + Hvis du sletter samtalen, blir den også slettet for alle andre deltakere. + Slett melding + Meldingen ble slettet, men den kan ha blitt lekket til andre tjenester + Bruker %1$s ble fjernet + Fjern moderatorstatus + Ta opp talemelding + Send melding + Nåværende konto + Server + Servervarslingsapp installert? + Bruker + Brukerstatus aktivert? + Android-versjon + App + App-navn + Registrerte brukere + App-versjon + Batterioptimalisering ignoreres, alt er bra + Batteri innstillinger + Enhet + Åpne sjekkliste for feilsøking + Åpne diagnoseskjermen + Åpne dontkillmyapp.com + Siste push-tokenhenting fra Firebase + Siste push-tokengenerering fra Firebase + Ingen firebase push-token angitt. Vennligst opprett en feilrapport. + Firebase push-token + Google Play-tjenester er ikke tilgjengelige. Varslinger støttes ikke. + Google Play-tjenester + Google Play-tjenester er tilgjengelige + Siste push-registrering på push proxy + Ikke registrert enda på push proxy + Siste push-registrering på serveren + Ikke registrert enda på serveren + Metainformasjon + Generering av systemrapport + Anropsvarslingskanal aktivert? + Varslingskanal for meldinger aktivert? + Varslingstillatelser + Telefon + Server Talk-versjon + Serverversjon + Ekstern + Innvendig + Signaleringsmodus + Ugyldig passord + Server er for øyeblikket i vedlikeholdsmodus. + App er utdatert + Appen er for gammel og støttes ikke lenger av denne serveren. Vennligst oppdater. + Oppdater + Vil du autorisere eller slette denne kontoen på nytt? + Hvis du lagrer dette mediet på lagring, får alle andre apper på enheten din tilgang til det. + Fortsette? + Nei + Lagre til lagring? + Ja + Kunne ikke hente visningsnavn, avbryter + Kunne ikke lagre visningsnavn, avbryter + Rediger + Rediger + Rediger melding + Redigert av administrator + Timeplan + 8 timer + 4 uker + ett kvarter + 1 dag + 1 time + 1 uke + Utløpe chatmeldinger + Chatmeldinger kan utløpe etter en viss tid. Merk: filer som deles i chat, slettes ikke for eieren, men deles ikke lenger i samtalen. + Feilet ved henting av settinger for signalering + Aksepter + Avvis + fra %1$s på %2$s + Ingen ventende invitasjoner + Du har ventende invitasjoner + Tilbake + Tillatelse til filtilgang kreves + Bruker følger en offentlig lenke + Du: %1$s + Fremover + Videresender til … + Galleri + Har du ingen server enda?\nKlikk her for å opprette en hos en tilbyder + Få kildekoden + Gruppe + Gjest + Gjestetilgang + Kan ikke aktivere/deaktivere gjestetilgang. + Tillat gjester å dele en offentlig lenke for å bli med i denne samtalen. + Tillat gjester + Skriv inn et passord + Gjestetilgang-passord + Feil under innstilling/deaktivering av passord. + Angi et passord for å begrense hvem som kan bruke den offentlige koblingen. + Passordbeskyttelse + Send invitasjoner på nytt + Invitasjoner ble ikke sendt på grunn av en feil. + Invitasjoner ble sendt ut igjen. + Del samtalekobling + Enter a message … + Batterioptimalisering ignoreres ikke. Dette bør endres for å sikre at varsler fungerer i bakgrunnen! Klikk OK og velg \"Alle apper\" -> %1$s -> Ikke optimaliser + Ignorer batterioptimalisering + Viktig samtale + Invitasjoner + Bli med i åpne samtaler + Behold + Du må forfremme en ny moderator før du kan forlate samtalen + %1$s | Sist endret: %2$s + Forlat samtale + Forlater samtalen... + GNU General Public License, Version 3 + Lisens + %s grensen for maks antall tegn er nådd + Lobby + Dette møtet er planlagt til %1$s + Møtet starter snart + Du venter for øyeblikket i lobbyen. + Dine nåværende posisjon + Plasseringstillatelse kreves + Posisjon ukjent + Låst + Trykk for å låse opp + Ikke satt + Merk som lest + Merk som ulest + Mislyktes + Sending av melding feilet: + Frakoblet + Avbryt svar + Melding lest + Melding sendt + For å aktivere talekommunikasjon, vennligst gi \"Mikrofon\" tillatelse. + Du har et tapt anrop fra %s + Moderator + Ny samtale + Synlighet + Uleste nevner + Uleste meldinger + %1$s ikke tilgjengelig (ikke installert eller begrenset av admin) + Gjest + Nei + Ingen åpne samtaler + Ingen åpne samtaler du kan bli med i.\nEnten er det ingen åpne samtaler, eller så har du allerede blitt med i dem alle. + Ingen mellomserver + Du har ikke lov til å aktivere lyd! + Du har ikke lov til å aktivere video! + Ikke nå + %1$s på %2$s merknadskanal + Samtaler + Varsle om innkommende anrop + Meldinger + Varsle om innkommende meldinger + Last opp + Varsle om fremdrift for opplasting + Innstillinger for notifiseringer + Varslingstillatelse og batteriinnstillinger er riktig konfigurert til å motta varsler. Hvis du uansett har problemer med å motta varsler, kan du sjekke om varslingskanalene for samtaler og meldinger er aktivert. Ytterligere hjelp finner du på DontKillMyApp.com eller på sjekklisten for feilsøking. Hvis dette ikke hjelper, kan du gå til diagnoseskjermen og sende en feilrapport. + Feilsøking for varsling + Alltid varsle + Varsle når nevnt + Aldri varsle + Offline, vennligst sjekk din nett forbindelse + OK + Også åpen for brukere av guest-appen + Eier + Deltakere + Legg til deltager + PassordP + Angi rettigheter + Noen rettigheter ble nektet. + Vennligst tillat rettigheter + Åpne innstillinger + Vennligst gi rettigheter under Innstillinger > Rettigheter + Konto ble ikke funnet + Chat via %s + Demp mikrofon + Aktiver mikrofon + Meldinger + Personvern + Personlig informasjon + Gi moderatorstatus + Offentlig samtale + Push notifisering deaktivert + Trykk-for-å-snakke + Med mikrofonen avslått, klikk & hold for å bruke klikk-for-å-snakke + Påminnn meg senere + Fjern fra favoritter + Fjern gruppe og medlemmer + Fjern deltaker + Fjern lag og medlemmer + Endre navn på samtale + Endre navn + Svar + Svar privat + Lagre + Lagret + 30 sekunder + 5 minutter + 1 minutt + 10 minutter + 600 + 60 + 30 + 300 + Søk + Tøm søk + Velg en konto + %1$s sendte en GIF. + Du sendte en GIF. + %1$s sendte en video. + Du sendte en video. + %1$s sendte en lyd. + Du sendte en lyd. + %1$s sendte et bilde. + Du sendte ett bilde. + Test servertilkobling + Oppgrader din %1$s-database + Klarte ikke å importere valgt konto + Koblingen til %1$s nettgrensesnitt når du åpner det i nettleseren. + Importer konto fra %1$s-appen + Importer konto + Importer kontoer fra %1$s-appen + Importer kontoer + Ta din %1$s ut av vedlikehold + Fullført din %1$s-installasjon + Tester forbindelsen + Serveren har ikke en støttet Talk app installert + Serveradresse https://… + %1$s fungerer bare med %2$s 13 og høyere + Angi nytt passord + Instillinger + Din allerede eksisterende konto ble oppdatert, istedenfor å legge til en ny + Avansert + Utseende + Samtaler + Åpne diagnoseskjermen for å sjekke innstillinger eller opprette feilrapport + Diagnose + Instruerer tastaturet om å deaktivere personlig læring (uten garantier) + Inkognito-tastatur + Ingen lyd + Samtale-appen er ikke installert på serveren du forsøkte å autentisere mot + Varsler + Varsler blir avvist + Varsler innvilges + Meldinger + Samsvar kontakter basert på telefonnummer for å integrere Talk-snarveien i systemkontaktappen + Feil 429 for mange forespørsler + Du kan angi telefonnummeret ditt slik at andre brukere kan finne deg + Skriv inn telefonnummer + Ugyldig telefonnummer + Telefonnummeret er angitt + Telefonnummer + Telefonnummerintegrasjon + Personvern + Mellomserver + Proxy-passord + Mellomserver port + Type mellomserver + Proxy-brukernavn + Del min lesestatus og vis andres lesestatus + Lesestatus + Autoriser kontoen på nytt + Fjern + Fjern konto + Bekreft at du vil fjerne den nåværende kontoen. + Lås %1$s med Android skjermlås eller støttet biometrisk metode. + Skjermlås forsinkelse + Skjermlås + Forhindrer skjermbilder i den siste listen og inne i appen + Skjerm sikkerhet + Serverversjonen er veldig gammel og støttes ikke i neste versjon! + Serverversjonen er for gammel og støttes ikke av denne versjonen av Android-appen + Ikke støttet server + Mørk + Bruk systemets standard + tema + Lys + Tema + Del min skrivestatus og vis andres skrivestatus + Skrivestatus er bare tilgjengelige når du bruker en HPB (High Performance Backend) + Skrivestatus + Proxy krever legitimasjon + Advarsel + Bare gjeldende konto kan reautoriseres + Del kontakt + Tillatelse til å lese kontakter kreves + Del gjeldende plassering + Del lenke + Del plassering + Del denne plasseringen + Velg konto + Delte elementer + Deck kort + Bilder, filer, talemeldinger... + Ingen delte elementer + Sted + Delt plassering + Sorter etter + Start tidspunkt + Bytt konto + Lag + Velg filer + Sende disse filene til %1$s? + Sende denne filen til %1$s? + Beklager, opplasting feilet + Opplasting av %1$s feilet + Feil + Del fra %1$s + Last opp fra enhet + Laster opp + %1$s til %2$s - %3$s\%% + Ta et bilde + Ta opp video + Bruker + Videoopptak fra %1$s + Taleopptak fra %1$s (%2$s) + Hold for å spille inn, slipp for å sende. + Tillatelse til lydopptak kreves + « Skyv for å avbryte + Webinar + Ja + Neste uke + Ingen telefonnummerintegrasjon på grunn av manglende rettigheter + Alle meldinger + kun @-nevner + Av + Forvalg + 1 time + Pålogget + Online-status + Åpne samtaler + Åpne i Filer-appen + Spill/pause talemelding + Legg til alternativ + Rediger stemme + Avslutt avstemning + Ønsker du virkelig å avslutte denne avstemningen? Det kan ikke angres. + Du kan ikke stemme med flere alternativer for denne avstemningen. + Flere svar + Valg + Privat avstemning + Spørsmål + Spørsmålet ditt + Resultater + Innstillinger + Stemme + Stemme sendt inn + Tidligere angitt + Løft hånden + Alle + Deling av filer fra lagring er ikke mulig uten tillatelser + Samtalen blir tatt opp + Avbryt opptaksstart + Opptaket feilet, vennligst kontakt administratoren din. + Start opptak + Ønsker du virkelig å stoppe opptaket? + Stopp samtaleopptak + Stopp opptak + Stopper opptaket... + Det kreves samtykke til opptak for alle samtaler + Opptaket kan inneholde stemmen din, video fra kamera og skjermdeling. Ditt samtykke kreves før du blir med i samtalen. Gir du samtykke? + Krev samtykke til opptak før du blir med i samtalen i denne samtalen + Samtykke til opptak + Samtalen kan bli tatt opp. + Innspilling + Fjernet samtalen%1$s fra favoritter + Samtalen %1$s ble omdøpt + Tilbakestill status + Det er ikke mulig å bli med i andre rom mens du er i en samtale + Lagre + Scan QR Code + Synkroniser kun til betrodde servere + Sammenknyttet + Kun synlig for personer i denne installasjonen og for gjester + Lokal + Kun synlig for personer som matches via telefonnummerintegrasjon via Talk på mobil + Privat + Synkroniser til betrodde servere og den globale og offentlige adresseboken + Publisert + Veksle omfang + Endre personvernnivå på %1$s + Rull til bunnen + Søkeikon + sekunder siden + Valgt + Send e-post + Send til + Du har ikke lov til å dele innhold til denne chatten + Send til... + Send uten varsel + Sett + Angi avatar fra kamera + Velg status + Velg statusmelding + Del + Bli med samtale %1$s på %2$s + Lyd + Fil + Media + Annet + Avstemning + Samtaleopptak + Svarer + Vis utestengelsesårsak + Vis utestengte deltakere + Favoritt + Du har ikke lov til å starte et anrop + startet et anrop + Statusmelding + Bytt til grupperom + Bytt til hovedrom + Ta et bilde + Feil ved å ta bilde + Å ta et bilde er ikke mulig uten tillatelser + Ta bilde på nytt + Send + Bytt kamera + Beskjær bilde + Reduser bildestørrelse + Lommelykt av/på + 30 minutter + Denne uken + Dette er en testmelding + Denne helgen + I dag + I morgen + Oversette + Oversettelse + Kopier oversatt tekst + Oppdag språk + Enhetsinnstillinger + Kunne ikke gjenkjenne srpåk + Oversettelse feilet + Fra + Til + og 1 annen skriver... + skriver... + skriver... + og %1$s andre skriver... + Opphev utestengelsen + Ulest + Last opp et nytt profilbilde fra enhet + Erstatter: + Bruker avatar + Adresse + Fullt navn + E-post + Telefonnummer + Twitter + Nettsted + Status + Kan ikke hente personlig brukerinformasjon. + Ingen personlig info satt + Legg til navn, bilde og kontaktdetaljer på profilsiden. + Hva er din status? + + %d stemme + %d stemmer + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..3623f75 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,67 @@ + + + + #0082C9 + #006AA3 + @color/colorPrimary + #ff6F6F6F + + + #1E1E1E + #FFFFFF + + + #ffffff + #deffffff + #99ffffff + #61ffffff + + + #8Affffff + + #121212 + #99121212 + + #FFFFFF + #121212 + + #00AA00 + + #373737 + #D8D8D8 + + #484848 + + + #33000000 + + + #121212 + #2A2A2A + #14FFFFFF + #3F3F73 + #313B75 + + #8c8c8c + + #29ffffff + + + #4B4B4B + #282828 + + #1FFFFFFF + #818181 + + #353535 + + #99FFFFFF + + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000..c2a3dbb --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,702 @@ + + + Bewerken + Toevoegen + Toevoegen aan Notities + Gesprek %1$s toegevoegd aan favorieten + Zoeken in %s + Toon afwezig + Archiveer gesprek + Wanneer een gesprek wordt gearchiveerd is het standaard verborgen. Selecteer de filter \"Gearchiveerd\" om gearchiveerdde gesprekken te zien. Directe vermeldingen worden nog steeds ontvangen. + Gearchiveerd + %1$s gearchiveerd + Audio oproep + Bluetooth + Audio output + Telefoon + Speaker + Bedrade headset + Uw status is automatisch ingesteld + Avatar + Afwezig + Terug-knop + Blokkeer + Blokkeer deelnemer + Blokkeerlijst + Bezet + Agenda + Geavanceerde oproepopties + Dit gesprek is actief gedurende een uur + Oproepen zonder melding + Toegang tot camera verleend. Kies aub. uw camera opnieuw. + Annuleer login + Kies een avatar uit de cloud + Statusbericht wissen + Statusbericht wissen na + Sluit + Sluiten-pictogram + Verbinding tot stand gebracht + Geen verbinding met server + Verbinding verbroken - Verzonden berichten in de wachtrij geplaatst + Zet opname vast voor doorlopende opname van de spraak + Gesprek is gearchiveerd + Gesprek is alleen-lezen + Gesprek op alleen-lezen instellen is mislukt + Gesprekken + Maak gesprek + Maak issue + Aangepast + Gevarenzone + %1$s in %2$s + Verwijder avatar + Gesprek %1$s verwijderd + Niet storen + Niet opruimen + Bewerken + Berichten ouder dan 24 uur kunnen niet worden bewerkt + Bewerk bericht + Recent + Versleuteld + Beëindig gesprek voor iedereen + Het laden van uw gesprekken was problematisch + Fout opgetreden bij deblokkeren deelnemer + Kon %1$s niet opslaan + 15 minuten + map + Laden … + %1$s (%2$d) + 4 uur + Kon wachtende uitnodigingen niet ophalen + (bewerkt) + Interne notitie + Onzichtbaar + Talen konden niet opgehaald worden + Ophalen mislukt + Later vandaag + Verlaat oproep + Je hebt gesprek %1$s verlaten + Laad meer resultaten + Lokale tijd: %1$s + Gesprek vergrendelen + Slot symbool + Hand omlaag + Gesprek %1$s gemarkeerd als gelezen + Gesprek %1$s gemarkeerd als ongelezen + Vermeld + Nieuwste eerst + Oudste eerst + A - Z + Z - A + Grootste eerst + Kleinste eerst + Bericht gekopieerd + Bericht verwijderd door jou + Bewerkt door %1$s + Klik om de peiling te openen + Geen zoekresultaten + Begin met typen om te zoeken + Zoeken ... + Berichten + Onderdruk alle meldingen + Het geselecteerde account is nu geïmporteerd en beschikbaar + Over + Actieve gebruiker + Toevoegen account + Het account is gemarkeerd voor verwijdering en kan niet worden gewijzigd + Openen hoofdmenu + Toevoegen bijlage + Toevoegen emojis + Toevoegen aan gespek + Toevoegen deelnemers + Toevoegen aan favorieten + OK, alles in orde! + Pin: %1$s + Ontgrendel %1$s + Om bluetooth speakers te activeren, activeer de \"Apparaten in de buurt\" machtiging. + Als videogesprek opnemen + Als geluidsgesprek opnemen + Wijzig de oudio uitgang + Omschakelen camera + Ophangen + Microfoon omschakelen + Open beeld-in-beeld modus + Omschakelen naar zelf-video + INKOMEND + Gespreksnaam + Oproepmeldingen + %1$s stak een hand op + Opnieuw verbinden … + BELLEN + %1$s in gesprek + %1$s met telefoon + %1$s met video + Geen reactie binnen 45 seconden, tap om het opnieuw te proberen. + %s oproep + %s videogesprek + %s gesprek + Om videogesprekken te houden, verleen \"Camera\" machtiging. + Annuleren + Kon mogelijkheden niet ophalen, afbreken + Bijschrift + Vertrouw je het tot nu onbekende SSL certificaat, uitgegeven door %1$s voor %2$s, geldig van %3$s tot %4$s? + Controleer het certificaat + Je SSL-instellingen blokkeerden de verbinding + Authenticatiecertificaat veranderen + Wijzig wachtwoord + Annuleer bewerken + Annuleer bewerken + Alle berichten verwijderen + Alle berichten zijn verwijderd + Wil je echt dit alle andere berichten in dit gesprek echt verwijderen? + Wijzig clientcertificaat + Instellen clientcertificaat + en + Kopiëren + Gekopieerd naar het klembord + Creëer + Uitgeschakeld + Wissen + Sorry, er is iets fout gegaan! + Meer opties + Instellen + Overslaan + Onbekend + Selecteer authenticatiecertificaat + Verbinden … + Gereed + Gespreksbeschrijving + Gespreksinformatie + Videogesprek + Gesprek + Gesprek niet gevonden + Gespreksinstellingen + Doe mee met een gesprek, of start een nieuw + Zeg hallo tegen je vrienden en collega\'s! + Kopie + Maak een nieuw gesprek + Peiling aanmaken + Jij: + Vandaag + Gisteren + Verwijderen + Alles verwijderen + Verwijder gesprek + Als je het gesprek verwijdert, wordt het ook verwijderd voor alle andere deelnemers. + Bericht verwijderen + Het bericht is verwijderd, maar het is mogelijk gelekt naar andere services + Verwijder nu + Gebruiker %1$s is verwijderd + Degradeer van moderator + Opnemen gesproken bericht + Verstuur bericht + Huidige account + Server + Server notificatie-app geïnstalleerd? + Gebruiker + Gebruikersstatus ingeschakeld? + Android versie + App + App naam + Geregistreerde gebruikers + App versie + Batterijoptimalisatie is uitgeschakeld, prima + Batterijoptimalisatie is ingeschakeld, dit kan problemen veroorzaken. Je zou batterijoptimalisatie moeten uitschakelen! + Batterij instellingen + Apparaat + Open de probleemoplossingschecklist + Open diagnosevenster + Open dontkillmyapp.com + Laatste keer ophalen firebase push token + Laatste keer genereren firebase push token + Geen firebase push token ingesteld. Maak een bug report a.u.b. + Firebase push token + Google Play services zijn niet beschikbaar. Meldingen zijn niet mogelijk + Google Play services + Google Play services zijn beschikbaar + Laatste push-registratie bij push proxy + Nog niet geregistreerd bij push proxy + Laatste push-registratie bij server + Nog niet geregistreerd bij server + Meta-informatie + Systeemrapportage maken + Gespreksmeldingenkanaal ingeschakeld? + Berichtmeldingenkanaal ingeschakeld? + Meldingsrechten + Telefoon + Server Talk versie + Serverversie + Extern + Intern + Signaling modus + Ongeldig wachtwoord + Server bevindt zich momenteel in onderhoudsmodus + App is verouderd + De app is te oud en wordt niet langer ondersteund door de huidige server. Update a.u.b. + Update + Wil je dit account opnieuw authenticeren of verwijderen? + Het opslaan van deze media naar de lokale opslag betekent dat deze benaderbaar is voor andere apps op je apparaat. + Doorgaan? + Nee + Opslaan naar opslag? + Ja + Weergave kon niet worden opgehaald, afgebroken + Kon weergavenaam niet opslaan, afbreken + Bewerken + Bewerken + Bewerk bericht + Bewerkt door admin + Menu voor evenementgesprekken + Agenda + 8 uur + 4 weken + Uit + 1 dag + 1 uur + 1 week + Laat chatberichten vervallen + Chatberichten kunnen na een bepaalde tijd verlopen. Noot: Bestanden die gedeeld zijn in een chat worden niet verwijderd door de eieganaar, maar worden niet langer gedeeld in het gesprek. + Kon signaleringsinstellingen niet ophalen + Accepteren + Afkeuren + van %1$s op %2$s + Geen wachtende uitnodigingen + Je hebt wachtende uitnodigingen + Terug + Toestemming voor bestandstoegang is vereist + Filter gesprekken + Gebruiker die een openbare link volgde + Jij: %1$s + Doorsturen + Doorsturen aan … + Galerij + Heb je nog geen server?\n +Kies er eentje van een provider. + Download sourcecode + Groep + Gast + Gasttoegang + Kan gasttoegang niet in-/uitschakelen. + Sta gasten toe een openbare link te gebruiken om deel te nemen aan dit gesprek. + Sta gasten toe + Voer een wachtwoord in + Wachtwoord gasttoegang + Fout bij instellen/uitschakelen wachtwoord + Stel een wachtwoord in om te beperken wie de openbare link kan gebruiken. + Wachtwoordbeveiliging + Opnieuw verzenden uitnodigingen + Uitnodigingen zijn niet verstuurd vanwege een fout + Uitnodigingen werden opnieuw verstuurd. + Deel gesprekslink + Voer een bericht in… + Batterijoptimalisatie staat niet uit. Dit moet aangepast worden zodat meldingen op de achtergrond werken! Druk op OK en selecteer \"Alle apps\" -> %1$s -> Niet optimaliseren + Batterijoptimalisatie negeren + Belangrijk gesprek + De gebruikersstatus ‘Niet storen’ wordt genegeerd voor belangrijke gesprekken + Ongeldige tijd + Uitnodigingen + Deelnemen aan open gesprekken + Behouden + Je moet een nieuwe moderator aanwijzen voordat je het gesprek kan verlaten + %1$s | Laatst gewijzigd: %2$s + Verlaat gesprek + Verlaten gesprek … + GNU General Public Licence, versie 3 + Licentie + match +%stekens limiet bereikt + Lobby + Deze vergadering staat gepland voor %1$s + Deze vergadering zal binnenkort starten + Je wacht nu in de lobby. + Je huidige locatie + Locatiemachtiging is vereist + Positie onbekend + Vergrendeld + Tikken om te ontgrendelen + Niet ingesteld + Markeren als gelezen + Markeren als ongelezen + Gesprek gemarkeerd als belangrijk + Gesprek niet gemarkeerd als gevoelig + Gesprek gemarkeerd als gevoelig + Gesprek niet gemarkeerd als belangrijk + Bespreking is beëindigd + Bericht toegevoegd aan notities + Mislukt + Fout bij versturen bericht: + Offline + Antwoord annuleren + Bericht gelezen + Verzenden + Bericht verstuurd + Om videogesprekken te houden, verleen \"Microfoon\" machtiging. + Je hebt een gesprek gemist van %s + Moderator + Nieuw gesprek + Zichtbaarheid + Ongelezen vermeldingen + Ongelezen berichten + %1$s niet beschikbaar (niet geïnstalleerd of beperkt door beheerder) + Gast + Nee + Geen open gesprekken + Geen open gesprekken waaraan je kan deelnemen.\nEr zijn geen open gesprekken of je bent al lid van alle gesprekken. + geen proxy + Je bent niet geautoriseerd om audio in te schakelen + Je bent niet geautoriseerd om video in te schakelen + Niet nu + %1$s op het %2$s notificatiekanaal + Gesprekken + Melden inkomende gesprekken + Berichten + Melden over inkomende gesprekken + Uploads + Informeer over uploadvoortgang + Meldingsinstellingen + Meldingen zijn niet goed ingesteld + Meldingstoestemming en batterij-instellingen zijn goed ingesteld. Als je toch problemen hebt met het ontvangen van meldingen, controleer dan of de meldingskanalen voor gesprekken en berichten ingeschakeld zijn. Meer hulp is te vinden op DontKillMyApp.com of via de probleemoplossingschecklist. Als dit niet helpt, ga dan naar het diagnosevenster en stuur een bug report. + Meldingen probleemoplossing + Altijd waarschuwen + Waarschuwen bij vermelding + Nooit waarschuwen + Op dit moment offline, controleer je verbinding + OK + Lopende bespreking + Open gesprek voor geregistreerde gebruikers + Ook open voor gastgebruikers van de app + Eigenaar + Deelnemers + Deelnemers toevoegen + Wachtwoord + Machtigingen instellen + Enkele machtigingen zijn geweigerd. + Sta machtigingen toe a.u.b. + Openen instellingen + Verleen machtigingen via Instellingen > Machtigingen + Account niet gevonden + Chat via %s + Microfoon dempen + Microfoon inschakelen + Berichten + Privacy + Persoonlijke info + Promoveer tot moderator + Openbaar gesprek + Push meldingen uitgeschakeld + Er is iets misgegaan, fout is %1$s + Sorry, er is iets misgegaan, het testbericht kan niet worden opgehaald. + De pushmelding is succesvol verzonden. Je zou nu een melding moeten ontvangen op dit apparaat met de titel ‘Pushmeldingen testen’. + Druk-om-te-praten + Met uitgeschakelde microfoon, klik&indrukken om Druk-om-te-praten te gebruiken + Herinner me later + Verwijderen uit favorieten + Verwijderen groep en leden + Verwijder deelnemer + Verwijder wachtwoord + Verwijderen team en leden + Hernoem gesprek + Hernoemen + Antwoord + Antwoord privé + De ruimte is met succes gereserveerd + Opslaan + Succesvol opgeslagen + 30 seconden + 5 minuten + 1 minuut + 10 minuten + Onmiddellijk + 600 + 60 + 30 + 300 + Zoeken + Wis zoekvak + Selecteer een account + Bijwerken bericht + Gevoelig gesprek + Berichtvoorvertoning wordt uitgeschakeld in de gesprekslijst en meldingen + %1$s verstuurde een GIF. + Je verstuurde een GIF. + %1$s verstuurde een video. + Je verstuurde een video. + %1$s verstuurde een audio. + Je verstuurde een geluidsbestand. + %1$s verstuurde een afbeelding + Je verstuurde een afbeelding. + %1$s stuurde een deck kaart + Testen serververbinding + Werk je %1$s database bij + Kon het geselecteerde account niet importeren + De link naar je %1$s web interface wanneer je die opent in de browser. + Importeren account van de %1$sapp + Importeer account + Importeren accounts van de %1$sapp + Importeer accounts + Haal je %1$s uit de onderhoudsmodus + Beëindig je %1$s installatie + Testen verbinding + De server heeft geen ondersteunde Talk app geïnstalleerd + Serveradres https://… + %1$s werkt alleen met %2$s 13 en hoger + Instellen nieuw wachtwoord + Instellen wachtwoord + Instellingen + Je al bestaande account werd bijgewerkt, in plaats van een nieuwe aan te maken + Geavanceerd + Uiterlijk + Oproepen + Neem contact op met de beheerder van + Open het diagnosevenster om de instellingen te controleren of een bug report te maken + Diagnose + Instrueert het toetsenbord gepersonaliseerd leren uit te schakelen (geen garanties) + Incognito keyboard + Geen geluid + De Talk-app is niet geïnstalleerd op de server waartegen je probeert te authenticeren + Meldingen + Meldingen zijn afgewezen + Meldingen zijn toegestaan + Berichten + Match contactpersonen op telefoonnummer om Talk-snelkoppelingen te integreren in je telefoonboek + Fout 429 Teveel aanvragen + Je kunt je telefoonnummer instellen zodat andere gebruikers je kunnen vinden + Invoeren telefoonnummer + Ongeldig telefoonnummer + Telefoonnummer succesvol ingesteld + Telefoonnummer + Telefoonnummer-integratie + Privacy + Proxy host + Proxy wachtwoord + Proxy poort + Proxy type + Proxy gebruikersnaam + Deel mijn lees-status en toon de lees-status van anderen + Lees-status + Account opnieuw autoriseren + Verwijderen + Verwijder account + Bevestig dat je het huidige account wilt verwijderen. + Vergrendel %1$s via de Android schermvergendeling of een ondersteunde biometrische methode + Schermblokkade inactiviteit time-out + Schermvergrendeling + Voorkomt schermafdrukken in de recente lijsten en binnen de app, + Schermbeveiliging + De serverversie is heel oud en wordt niet meer ondersteund in de volgende release. + De serverversie is te oud en wordt niet meer ondersteund door deze versie van de Android app + Niet-ondersteunde server + Server notificatie-app niet geïnstalleerd + Ingesteld door batterijbespaarder + Donker + Gebruik systeem standaarden + thema + Licht + Thema + Deel mijn typen-status en toon de typen-status van anderen + Typen-status is alleen beschikbaar als een high performance backend (HPB) gebruikt wordt + Typen-status + Proxyserver heeft inloggegevens nodig + Waarschuwing + Alleen het huidige account kan opnieuw worden geautoriseerd + Deel contact + Machtiging om contacten te lezen is vereist + Deel huidige locatie + Deel link + Deel locatie + Deel deze locatie + Kies account + Gedeelde objecten + Deck kaart + Plaatjes, bestanden, spraakberichten ... + Geen gedeelde objecten + Locatie + Gedeelde locatie + Als meldingen niet goed ingesteld zijn, toon dan een terugkerende waarschuwing + Toon terugkerende meldingswaarschuwing + Sorteren op + Start groepsgesprek + Begintijd + Account wisselen + Team + Test pushnotificatie + Test resultaten + Vandaag om %1$s + Morgen om %1$s + Kies bestanden + Bestanden versturen naar %1$s? + Bestand versturen naar %1$s? + Sorry, uploaden mislukt + Upload %1$s mislukt + Gefaald + Delen van %1$s + Uploaden vanaf apparaat + Uploaden + %1$s naar%2$s - %3$s\%% + Neem foto + Maak video + Gebruiker + Video-opname van %1$s + Talk neemt op vanaf%1$s (%2$s) + Ingedrukt houden om op te nemen, loslaten om te versturen. + Toestemming voor audio-opname is vereist + « Schuiven om te annuleren + Webinar + Ja + Volgende week + Geen gearchiveerde gesprekken + Geen offline berichten bewaard + Geen telefoonnummer-integratie wegens ontbrekende machtigingen + Standaard + 1 uur + Online + Online status + Open gesprekken + Openen in de Bestanden-app + Open notities + Afspelen/pauzeren voicebericht + Controle afspeelsnelheid + Voeg een optie toe + Bewerk stem + Peiling beëindigen + Wil je deze peiling echt beëindigen? Dit kan niet ogedaan gemaakt worden. + Je kan niet stemmen met meer opties voor deze peiling. + Meerdere antwoorden + Verwijder optie %1$d + Optie %1$d + Opties + Privé peiling + Vraag + Uw vraag + Resultaten + Instellingen + Stemmen + Stem verstuurd + Eerder ingesteld + Hand opsteken + Alle + Bestanden delen vanuit opslag is niet mogelijk zonder machtiging + Het gesprek wordt opgenomen + Annulleer start opname + The opname is mislukt. Neem contact op met je beheerder. + Begin opname + Wil de opname echt beëindigen? + Beëindig opname van gesprek + Beëindig opname + Opname beëindigen ... + Toestemming voor opname is vereist voor alle gesprekken + De opname kan je stem, video van je camera en schermdelen bevatten. Je toestemming is nodig voorafgaand aan deelname aan het gesprek. Geef je toestemming? + Toestemming voor opname vereist voor deelname aan dit gesprek + Toestemming opname + Het gesprek kan opgenomen worden. + Opnemen + Gesprek %1$s verwijderd uit favorieten + Gesprek %1$s is hernoemd + Opnieuw verzenden + Reset status + Het is niet mogelijk een ruimte binnen te gaan tijdens een gesprek + Bewaren + Scan QR Code + Alleen synchroniseren met vertrouwde servers + Gefedereerd + Alleen zichtbaar voor gebruikers op deze server en gasten + Lokaal + Alleen zichtbaar voor mensen die via telefoonnummerintegratie gematcht zijn voor Talk op mobiel + Privé + Synchroniseren met vertrouwde servers en het wereldwijde en openbare adresboek + Gepubliceerd + Scope omschakelen + Wijzigen privacyniveau van %1$s + Scroll naar beneden + Zoeken-pictogram + seconden geleden + Geselecteerd + Verstuur e-mail + Versturen naar + Je bent niet geautoriseerd inhoud te delen in deze chat + Versturen naar … + Versturen zonder melding + Stel in + Maak avatar met camera + Instellen status + Statusbericht instellen + Delen + Neem deel aan gesprek %1$s op %2$s + Geluid + Bestand + Media + Andere + Peiling + Gesprek wordt opgenomen + Stem + Toon reden uitsluiten + Toon uitgesloten deelnemers + Favoriet + U hebt geen toestemming om een gesprek te starten + een gesprek gestart + Statusbericht + Status omgezet + Schakel naar aparte ruimte + Schakel naar hoofdruimte + Neem een foto + Fout bij het nemen van een foto + Een foto nemen is niet mogelijk zonder machtiging + Herneem foto + Verzenden + Andere camera + Crop foto + Reduceer foto resolutie + Schakel zaklamp + 30 minuten + Deze week + Dit is een testbericht + Dit weekend + Vandaag + Morgen + Vertaal + Vertaling + Kopieer vertaalde tekst + Detecteer taal + Apparaatinstellingen + Kan taal niet detecteren + Vertaling mislukt + Van + Naar + en 1 ander is aan het typen ... + zijn aan het typen ... + is aan het typen + en %1$s anderen zijn aan het typen … + Dearchiveer gesprek + Als een gesprek gedearchiveerd is, wordt het standaard weer getoond. + %1$s uit archief gehaald + Uitsluiten opheffen + Ongelezen + Upload nieuwe avatar van apparaat + %1$s is niet op kantoor en kan mogelijk niet reageren + %1$s is vandaag niet op kantoor + Vervanging: + Gebruik avatar + Adres + Volledige naam + E-mailadres + Telefoonnummer + Twitter + Website + Status + Kon persoonlijke informatie niet ophalen. + Geen persoonlijke informatie ingesteld + Voeg naam, foto en contactgegevens toe op je profielpagina. + Videogesprek + Wat is jouw status? + + Zie %d gelijke bericht + Zie %d gelijke berichten + + + Dit gesprek wordt automatisch verwijderd voor iedereen na %1$d dag zonder activiteit + Dit gesprek wordt automatisch verwijderd voor iedereen na %1$d dagen zonder activiteit + + + %d stem + %d stemmen + + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000..c5b4f8f --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,735 @@ + + + Edycja + Dodaj + Dodaj do Notatek + Dodano rozmowę %1$s do ulubionych + Szukaj w %s + Wyglądaj jako offline + Archiwizuj rozmowę + Po zarchiwizowaniu rozmowa zostanie domyślnie ukryta. Wybierz filtr „Zarchiwizowane”, aby wyświetlić zarchiwizowane rozmowy. Bezpośrednie wzmianki będą nadal otrzymywane. + Zarchiwizowane + Zarchiwizowane %1$s + Rozmowa audio + Bluetooth + Wyjście audio + Telefon + Głośnik + Słuchawki przewodowe + Twój status został ustawiony automatycznie + Awatar + Bezczynny + Przycisk Wstecz + Blokowanie + Zablokuj uczestnika + Lista zablokowanych + Brak dostępności + Kalendarz + Zaawansowane opcje połączeń + Połączenie trwa od godziny. + Połącz bez powiadomienia + Udzielono dostępu do kamery. Proszę ponownie wybrać kamerę. + Anuluj logowanie + Wybierz awatar z chmury + Wyczyść komunikat statusu + Wyczyść komunikat statusu po + Zamknij + Ikona zamknięcia + Połączenie nawiązane + Brak połączenia z serwerem + Utracono połączenie - wysłane wiadomości są w kolejce + Zablokuj nagrywanie w celu ciągłego nagrywania wiadomości głosowej + Konwersacja została zarchiwizowana. + Rozmowa jest tylko do odczytu + Nie udało się ustawić rozmowy Tylko do odczytu + Rozmowy + Utwórz rozmowę + Utwórz zgłoszenie + Dowolnie + Strefa niebezpieczeństwa + %1$s w %2$s + Usuń awatar + Usuń nagranie głosowe + Usunięto rozmowę %1$s + Nie przeszkadzać + Nie czyść + Edytuj + Wiadomości starsze niż 24 godziny nie mogą być edytowane. + Edytuj wiadomość + Ostatnie + Zaszyfrowane + Zakończ połączenie + Zakończ połączenie dla wszystkich + Wystąpił problem z wczytaniem Twoich czatów + Wystąpił błąd podczas odblokowywania uczestnika + Nie udało się zapisać %1$s + 15 minut + katalog + Wczytywanie… + %1$s (%2$d) + Obserwowane wątki + 4 godziny + Nie udało się pobrać oczekujących zaproszeń + (edytowane) + Notatka wewnętrzna + Niewidoczny + Nie udało się pobrać języków + Pobieranie nie powiodło się + Później dzisiaj + Rozłącz się + Opuściłeś rozmowę %1$s + Wczytaj więcej wyników + Czas lokalny: %1$s + Odmowa dostępu do lokalizacji + Proszę włączyć w ustawieniach aplikacji + Usługi lokalizacji wyłączone + Proszę włączyć usługi lokalizacji (GPS), aby użyć tej funkcji + Zablokuj rozmowę + Symbol zamknięcia + Opuścić rękę + Oznaczono rozmowę %1$s jako przeczytaną + Oznaczono rozmowę %1$s jako nieprzeczytaną + Wspomniany + Od najnowszych + Od najstarszych + A - Z + Z - A + Od największych + Od najmniejszych + Wiadomość skopiowana + Czy na pewno chcesz usunąć tę wiadomość? + Wiadomość usunięta przez Ciebie + Edytowany przez %1$s + Dotknij, aby otworzyć sondę + Brak wyników wyszukiwania + Zacznij pisać, aby wyszukać… + Szukaj… + Wiadomości + Wycisz wszystkie powiadomienia + Wybrane konto jest teraz zaimportowane i dostępne + O aplikacji + Aktywny użytkownik + Dodaj konto + Konto jest zaznaczone do usunięcia, nie można wprowadzać zmian + Otwórz menu główne + Dodaj załącznik + Dodaj emoji + Dodaj do rozmowy + Dodaj uczestników + Dodaj do ulubionych + OK, wszystko gotowe! + Przypnij: %1$s + Odblokuj %1$s + Aby włączyć głośniki Bluetooth, należy udzielić dostępu dla \"Urządzenia w pobliżu\". + Odbierz jako połączenie wideo + Odbierz tylko jako połączenie głosowe + Zmień wyjście audio + Przełącz kamerę + Odłożyć słuchawkę + Przełącz mikrofon + Otwórz tryb obrazu w obrazie + Przełącz na własne wideo + PRZYCHODZĄCA + Nazwa rozmowy + Powiadomienia połączenia + %1$s podniósł rękę + Ponowne łączenie… + DZWONIENIE + %1$s w trakcie połączenia + %1$s z telefonem + %1$s z wideo + Brak odpowiedzi przez 45 sekund, dotknij, aby spróbować ponownie + %s połączenie + %s połączenie wideo + %s połączenie głosowe + Aby umożliwić komunikację wideo, należy udzielić dostępu dla \"Kamera\". + Anuluj + Błąd pobierania, przerwano żądanie + Podpis + Czy ufasz dotychczas nieznanemu certyfikatowi SSL, wydanemu przez %1$s dla %2$s, ważnemu od %3$s do %4$s? + Sprawdź certyfikat + Twoje ustawienia SSL uniemożliwiają połaczenie + Zmień certyfikat uwierzytelnienia + Zmień hasło + Anuluj edycję + Anuluj edycję + Usuń wszystkie wiadomości + Wszystkie wiadomości zostały usunięte + Czy na pewno chcesz usunąć wszystkie wiadomości z tej rozmowy? + Zmień certyfikat klienta + Skonfiguruj certyfikat klienta + i + Skopiuj + Kopiuj do schowka + Utwórz + Wyłączone + Odrzuć + Przepraszamy, coś poszło nie tak! + Więcej opcji + Ustaw + Pomiń + Nieznany + Wybierz certyfikat uwierzytelnienia + Łączę… + Gotowe + Opis rozmowy + Informacje o rozmowie + Rozmowa wideo + Rozmowa głosowa + Nie znaleziono rozmowy + Ustawienia rozmowy + Dołącz do rozmowy lub rozpocznij nową + Przywitaj się z przyjaciółmi i współpracownikami! + Kopiuj + Utwórz nową rozmowę + Utwórz sondę + Ty: + Dzisiaj + Wczoraj + Usuń + Usuń wszystko + Usuń rozmowę + Jeśli usuniesz rozmowę, zostanie ona również usunięta dla wszystkich pozostałych uczestników. + Usuń wiadomość + Wiadomość została pomyślnie usunięta, ale mogła przedostać się do innych usług + Usuń teraz + Użytkownik %1$s został usunięty + Zdegraduj z moderatora + Nagraj wiadomość głosową + Wyślij wiadomość + Bieżące konto + Serwer + Zainstalowano aplikację powiadamiającą serwer? + Użytkownik + Status użytkownika włączony? + Wersja Androida + Aplikacja + Nazwa aplikacji + Zarejestrowani użytkownicy + Wersja aplikacji + Optymalizacja baterii jest ignorowana, wszystko w porządku + Optymalizacja baterii jest włączona, co może powodować problemy. Powinieneś wyłączyć optymalizację baterii! + Ustawienia baterii + Urządzenie + Otwórz listę kontrolną rozwiązywania problemów + Otwórz ekran diagnostyczny + Otwórz stronę dontkillmyapp.com + Najnowszy pobrany token push firebase + Najnowszy wygenerowany token push firebase + Brak ustawionego tokenu push firebase. Utwórz raport o błędzie. + Token push firebase + Usługi Google Play są niedostępne. Powiadomienia nie są obsługiwane + Usługi Google Play + Dostępne są usługi Google Play + Najnowsza rejestracja push na serwerze push proxy + Jeszcze nie zarejestrowany w push proxy + Najnowsza rejestracja push na serwerze + Jeszcze nie zarejestrowany na serwerze + Informacje meta + Generowanie raportu systemowego + Kanał powiadomień o połączeniach włączony? + Kanał powiadomień wiadomości włączony? + Uprawnienia do powiadomień + Telefon + Wersja serwera Talk + Wersja serwera + Zewnętrzny + Wewnętrzny + Tryb sygnalizacji + Nieprawidłowe hasło + Serwer jest obecnie w trybie konserwacji. + Aplikacja jest przestarzała + Aplikacja jest zbyt stara i nie jest już obsługiwana przez ten serwer. Proszę zaktualizować. + Aktualizuj + Czy chcesz ponownie autoryzować lub usunąć konto? + Zapisanie tych multimediów do pamięci umożliwi dostęp do nich innym aplikacjom na Twoim urządzeniu. + Kontynuować? + Nie + Zapisać w pamięci? + Tak + Nie udało się pobrać nazwy, przerwano żądanie + Nie można zapisać wyświetlanej nazwy, przerwano żądanie + Edycja + Edycja + Edytuj wiadomość + Edytowane przez administratora + Menu rozmowy wydarzenia + Harmonogram + 8 godzin + 4 tygodnie + Wyłączona + 1 dzień + 1 godzina + 1 tydzień + Wygasa wiadomości na czacie + Wiadomości na czacie mogą wygasnąć po pewnym czasie. Uwaga: pliki udostępnione na czacie nie zostaną usunięte dla właściciela, ale nie będą już udostępniane w rozmowie. + Nie udało się pobrać ustawień sygnałowych + Akceptuj + Odrzuć + od %1$s o %2$s + Brak oczekujących zaproszeń + Masz oczekujące zaproszenia + Wstecz + Wymagane jest zezwolenie na dostęp do plików + Filtruj rozmowy + Użytkownik korzysta z łącza publicznego + Ty: %1$s + Przekaż dalej + Przekaż do… + Galeria + Nie masz jeszcze serwera?\nKliknij tutaj, aby uzyskać jeden od dostawcy + Pobierz kod źródłowy + Grupa + Gość + Dostęp dla gościa + Nie można włączyć/wyłączyć dostępu dla gościa. + Zezwalaj gościom na udostępnianie linku publicznego, aby dołączyć do tej rozmowy. + Zezwalaj gościom + Wprowadź hasło + Hasło dostępu gościa + Błąd podczas ustawiania/wyłączania hasła. + Ustaw hasło, aby ograniczyć dostęp do linku publicznego. + Ochrona hasłem + Wyślij ponownie zaproszenia + Zaproszenia nie zostały wysłane z powodu błędu. + Zaproszenia zostały ponownie wysłane. + Udostępnij link do rozmowy + Wpisz wiadomość… + Optymalizacja baterii nie jest ignorowana. Należy to zmienić, aby mieć pewność, że powiadomienia działają w tle! Kliknij OK i wybierz \"Wszystkie aplikacje\" -> %1$s -> Nie optymalizuj + Zignoruj optymalizację baterii + Ważna rozmowa + Status użytkownika „Nie przeszkadzać” jest ignorowany dla ważnych rozmów. + Nieprawidłowy czas + Zaproszenia + Dołącz do otwartych rozmów + Pozostaw + Zanim opuścisz rozmowę, musisz wybrać nowego moderatora + %1$s | Ostatnio zmodyfikowany: %2$s + Opuść rozmowę + Opuszczanie połączenia… + GNU General Public License, Version 3 + Licencja + Osiągnięto limit %s znaków + Poczekalnia + Spotkanie jest zaplanowane na %1$s + Spotkanie rozpocznie się wkrótce + Aktualnie czekasz w poczekalni. + Twoja aktualna lokalizacja + wymagane jest pozwolenie na lokalizację + Pozycja nieznana + Zablokowany + Dotknij, aby odblokować + Nie ustawiony + Oznacz jako przeczytane + Oznacz jako nieprzeczytane + Rozmowa oznaczona jako ważna + Rozmowa została odznaczona jako wrażliwa + Rozmowa została odznaczona jako wrażliwa + Rozmowa została odznaczona jako wrażliwa + Spotkanie zakończone + Wiadomość dodana do notatek + Nie powiodło się + Nie udało się wysłać wiadomości: + Niedostępny + Anuluj odpowiedź + Wiadomość przeczytana + Wysyłanie + Wiadomość wysłana + Mikrofon jest włączony i trwa nagrywanie dźwięku + Aby włączyć komunikację głosową, należy udzielić dostępu dla \"Mikrofon\". + Nie odebrałeś połączenia od %s + Moderator + Nowa rozmowa + Widoczne + Nieprzeczytane wzmianki + Nieprzeczytane wiadomości + %1$s niedostępne (niezainstalowane lub ograniczone przez administratora) + Gość + Nie + Brak otwartych rozmów + Brak otwartych rozmów, do których możesz dołączyć.\nAlbo nie ma otwartych rozmów lub już do nich dołączyłeś. + Brak proxy + Nie możesz aktywować dźwięku! + Nie możesz aktywować wideo! + Nie teraz + %1$s na %2$s kanale powiadomień + Połączenia + Powiadamiaj o połączeniach przychodzących + Wiadomości + Powiadamiaj o wiadomościach przychodzących + Wysłane + Powiadamiaj o postępie wysyłania + Ustawienia powiadomień + Powiadomienia nie są poprawnie skonfigurowane + Zezwolenie na powiadomienia i ustawienia baterii są prawidłowo skonfigurowane do otrzymywania powiadomień. Jeśli mimo to masz problemy z otrzymywaniem powiadomień, sprawdź, czy kanały powiadomień dla połączeń i wiadomości są włączone. Dalszą pomoc można znaleźć na stronie DontKillMyApp.com lub na liście rozwiązywania problemów. Jeśli to nie pomoże, przejdź do ekranu diagnostycznego i wyślij raport o błędzie. + Rozwiązywanie problemów z powiadomieniami + Zawsze powiadamiaj + Powiadom, gdy wspomniano + Nigdy nie powiadamiaj + Obecnie jesteś w offline, sprawdź swoją łączność + OK + Trwające spotkanie + Otwórz rozmowę dla zarejestrowanych użytkowników + Otwarte również dla gości aplikacji + Właściciel + Uczestnicy + Dodaj uczestników + Hasło + Ustaw uprawnienia + Odmówiono niektórych uprawnień. + Zezwalaj na uprawnienia + Otwórz ustawienia + Nadaj uprawnienia w Ustawienia > Uprawnienia + Nie znaleziono konta + Rozmawiaj przez %s + Wycisz mikrofon + Włącz mikrofon + Wiadomości + Prywatność + Informacje osobiste + Promuj na moderatora + Rozmowa publiczna + Powiadomienie push wyłączone + Przepraszamy, wystąpił błąd: %1$s + Przepraszamy, wystąpił błąd — nie można pobrać testowej wiadomości push. + Powiadomienie push zostało pomyślnie wysłane. Powinieneś teraz otrzymać powiadomienie na tym urządzeniu z tytułem „Testowanie powiadomień push”. + Naciśnij i mów + Gdy mikrofon jest wyłączony, kliknij i przytrzymaj, aby użyć funkcji \"Naciśnij i mów\" + Przypomnij mi później + Usuń z ulubionych + Usuń grupę i członków + Usuń uczestnika + Usuń hasło + Usuń zespół i członków + Zmień nazwę rozmowy + Zmień nazwę + Odpowiedz + Odpowiedz prywatnie + Pokój został pomyślnie zachowany + Zapisz + Zapisano pomyślnie + 30 sekund + 5 minut + 1 minuta + 10 minut + Natychmiastowe + 600 + 60 + 30 + 300 + Szukaj + Wyczyść wyszukiwanie + Wybierz konto + Zaktualizuj wiadomość + Wyślij nagranie głosowe + Rozmowa poufna + Podgląd wiadomości zostanie wyłączony na liście rozmów i w powiadomieniach. + %1$s wysłał GIF. + Wysłałeś GIF. + %1$s wysłał plik wideo. + Wysłałeś plik wideo. + %1$s wysłał plik dźwiękowy. + Wysłałeś plik dźwiękowy. + %1$s wysłał plik graficzny. + Wysłałeś plik graficzny. + %1$s wysłał kartę do tablicy + Sprawdź połączenie z serwerem + Zaktualizuj swoją %1$s bazę danych + Nie udało się zaimportować wybranego konta + Link do interfejsu internetowego %1$s, aby otworzyć w przeglądarce. + Importuj konto z aplikacji %1$s + Importuj konto + Importuj konta z aplikacji %1$s + Importuj konta + Przeprowadź %1$s konserwację do końca + Zakończ instalację %1$s + Testowanie połączenia + Serwer nie obsługuje zainstalowanej aplikacji Talk + Adres serwera https://… + %1$s działa tylko z %2$s 13 i nowszym + Ustaw nowe hasło + Ustaw hasło + Ustawienia + Zamiast dodawać nowe, zaktualizowano Twoje istniejące konto + Zaawansowane + Wygląd + Połączenia + Proszę o kontakt z administratorem + Otwórz ekran diagnostyczny, aby sprawdzić ustawienia lub utworzyć raport o błędzie + Diagnoza + Wskaż klawiaturze, aby wyłączyła spersonalizowane uczenie (bez gwarancji) + Klawiatura incognito + Bez dźwięku + Aplikacja Talk nie jest zainstalowana na serwerze, na którym próbowałeś się uwierzytelnić + Powiadomienia + Powiadomienia są odrzucane + Powiadomienia są przyznawane + Wiadomości + Dopasuj kontakty na podstawie numeru telefonu, aby zintegrować skrót Talka z aplikacją systemową kontaktów + Błąd 429 zbyt wiele żądań + Możesz ustawić swój numer telefonu, aby inni użytkownicy mogli Ciebie znaleźć + Wpisz numer telefonu + Nieprawidłowy numer telefonu + Numer telefonu ustawiony pomyślnie + Numer telefonu + Integracja numeru telefonu + Prywatność + Host proxy + Hasło proxy + Port proxy + Typ proxy + Nazwa użytkownika proxy + Udostępnij mój status odczytu i pokaż innym + Status odczytu + Ponownie autoryzuj konto + Usuń + Usuń konto + Potwierdź zamiar usunięcia bieżącego konta. + Blokuje %1$s za pomocą blokady ekranu Androida lub obsługiwanej metody biometrycznej + Czas bezczynności do blokady ekranu + Blokada ekranu + Zapobiega zrzutom ekranu ostatniej listy i wnętrza aplikacji + Zabezpieczenie ekranu + Wersja serwerowa jest bardzo stara i nie będzie obsługiwana w następnej wersji! + Wersja serwera jest zbyt stara i nie jest obsługiwana przez tę wersję aplikacji na Androida + Niewspierany serwer + Aplikacja do powiadamiania serwera nie została zainstalowana + Ustawione przez tryb oszczędzania baterii + Ciemny + Użyj domyślnych ustawień systemu + motyw + Jasny + Motyw + Udostępnij mój status pisania i pokaż status pisania innych osób + Status pisania jest dostępny tylko w przypadku korzystania z zaplecza o wysokiej wydajności (HPB) + Status pisania + Wymagane poświadczenie proxy + Ostrzeżenie + Tylko bieżące konto można ponownie autoryzować + Udostępnij kontakt + Wymagane uprawnienie do odczytu kontaktów + Udostępnij obecną lokalizację + Udostępnij link + Udostępnij lokalizację + Udostępnij tę lokalizację + Wybierz konto + Udostępnione elementy + Karta Deck + Obrazy, pliki, wiadomości głosowe… + Brak udostępnionych elementów + Lokalizacja + Udostępniona lokalizacja + Gdy powiadomienia nie są poprawnie skonfigurowane, wyświetlaj regularne ostrzeżenie + Pokaż regularne ostrzeżenie o powiadomieniu + Sortuj według + Rozpocznij czat grupowy + Czas rozpoczęcia + Przełącz konto + Zespół + Test powiadomień Push + Test wyników + Dziś o %1$s + Jutro o %1$s + Wybierz pliki + Wysłać te pliki do %1$s? + Wysłać ten plik do %1$s? + Wysyłanie nie powiodło się + Nie udało się wysłać %1$s + Awaria + Udostępnij z %1$sa + Wyślij z urządzenia + Wysyłanie + %1$s do %2$s - %3$s\%% + Zrób zdjęcie + Nagraj film wideo + Użytkownik + Nagrywanie filmu wideo z %1$s + Nagranie rozmowy z %1$s (%2$s) + Przytrzymaj, aby nagrać, zwolnij, aby wysłać. + Wymagane jest uprawnienie na nagrywanie dźwięku + « Przesuń, aby anulować + Webinarium + Tak + Następny tydzień + Brak zarchiwizowanych rozmów + Nie zapisano żadnych wiadomości offline + Brak integracji numeru telefonu z powodu braku uprawnień + Wszystkie wiadomości + Tylko, gdy @-wspomniano + Wyłączone + Domyślny + Śledź ustawienia rozmowy + 1 godzina + Online + Status online + Otwarte rozmowy + Otwórz w aplikacji Pliki + Otwórz Notatki + Przejdź do wątku + Odtwórz/wstrzymaj wiadomość głosową + Kontrola prędkości odtwarzania + Dodaj opcję + Edytuj głos + Zakończ sondę + Czy na pewno chcesz zakończyć sondę? Tego nie można cofnąć. + Nie możesz głosować z większą liczbą opcji w tej sondzie. + Wiele odpowiedzi + Usuń opcję %1$d + Opcja %1$d + Opcje + Sonda prywatna + Pytanie + Twoje pytanie + Wyniki + Ustawienia + Wyniki głosowania + Oddano głos + Ustawione wcześniej + Nie można odczytać kodu QR + Podnieść rękę + Wszystkie + Udostępnianie plików z magazynu nie jest możliwe bez uprawnień + Ostatnie wątki + Rozmowa jest nagrywana + Anuluj rozpoczęcie nagrywania + Nagrywanie nie powiodło się. Skontaktuj się z administratorem. + Rozpocznij nagrywanie + Czy na pewno chcesz zatrzymać nagrywanie? + Zatrzymaj nagrywanie rozmowy + Zatrzymaj nagrywanie + Zatrzymywanie nagrywania… + W przypadku wszystkich rozmów wymagana jest zgoda na nagrywanie + Nagranie może obejmować Twój głos, wideo z kamery i udostępniony ekran. Przed dołączeniem do rozmowy wymagana jest Twoja zgoda. Czy wyrażasz zgodę? + Wymagaj zgody na nagrywanie przed dołączeniem do tej rozmowy + Zgoda na nagrywanie + Rozmowa może zostać nagrana. + Nagranie + Usunięto rozmowę %1$s z ulubionych + Nazwa rozmowy %1$s została zmieniona + Wyślij ponownie + Zresetuj status + Podczas rozmowy nie można dołączać do innych pokoi + Zapisz + Scan QR Code + Synchronizuj tylko z zaufanymi serwerami + Sfederowane + Widoczne tylko dla osób w tej instancji i gości + Lokalne + Widoczne tylko dla osób dopasowanych poprzez integrację numeru telefonu za pomocą Talk na telefonie komórkowym + Prywatne + Synchronizuj z zaufanymi serwerami oraz globalną i publiczną książką adresową + Opublikowane + Przełącz zakres + Zmień poziom prywatności %1$s + Przewiń na dół + Ikona wyszukiwania + przed chwilą + Wybrany + Wyślij e-mail + Wyślij do + Nie możesz udostępniać treści na tym czacie + Wyślij do… + Wyślij bez powiadomienia + Ustaw + Ustaw awatar z aparatu + Ustaw status + Ustaw komunikat statusu + Udostępnij + Dołącz do rozmowy %1$s o godz. %2$s + Audio + Plik + Multimedia + Inne + Sonda + Nagrywanie rozmowy + Połączenie głosowe + Pokaż powód zablokowania + Pokaż zablokowanych uczestników + Ulubione + Nie możesz rozpocząć połączenia + Utwórz wątek + rozpoczął połączenie + Komunikat statusu + Status przywrócone + Przełącz się do pokoju podgrupy + Przełącz się do głównego pokoju + Zrobić zdjęcie + Błąd podczas robienia zdjęcia + Zrobienie zdjęcia nie jest możliwe bez uprawnień + Ponownie zrób zdjęcie + Wyślij + Przełącz kamerę + Przytnij zdjęcie + Zmniejsz rozmiar obrazu + Przełącz latarkę + 30 minut + W tym tygodniu + To jest wiadomość testowa + W ten weekend + Anuluj tworzenie wątku + Powiadomienia wątków + Odpowiedź + Tytuł wątku + Nie znaleziono wątków + Dzisiaj + Jutro + Tłumaczenie + Tłumaczenie + Kopiuj przetłumaczony tekst + Wykryj język + Ustawienia urządzenia + Nie można wykryć języka + Tłumaczenie nie powiodło się + Z + Na + i 1 inny pisze… + piszą… + pisze… + i %1$s innych pisze… + Przywróć archiwum rozmowy + Po przywróceniu archiwizacji rozmowa będzie ponownie wyświetlana domyślnie. + Niezarchiwizowane %1$s + Odblokuj + Nieprzeczytane + Wyślij nowy awatar z urządzenia + %1$s jest poza biurem i może nie odpowiadać + %1$s jest dzisiaj nieobecny w biurze + Zastępnik: + Awatar użytkownika + Adres + Pełna nazwa + E-mail + Numer telefonu + Twitter + Strona internetowa + Status + Nie udało się pobrać osobistych informacji o użytkowniku. + Brak informacji osobistych + Dodaj nazwę, zdjęcie profilowe i dane kontaktowe do swojego profilu. + Rozmowa wideo + Jaki jest Twój status? + + Zobacz %d podobną wiadomość + Zobacz %d podobne wiadomości + Zobacz %d podobnych wiadomości + Zobacz %d podobnych wiadomości + + + Ta konwersacja zostanie automatycznie usunięta dla wszystkich po %1$d dniu braku aktywności. + Ta konwersacja zostanie automatycznie usunięta dla wszystkich po %1$d dniach braku aktywności. + Ta konwersacja zostanie automatycznie usunięta dla wszystkich po %1$d dniach braku aktywności. + Ta konwersacja zostanie automatycznie usunięta dla wszystkich po %1$d dniach braku aktywności. + + + %d odpowiedź + %d odpowiedzi + %d odpowiedzi + %d odpowiedzi + + + %d głos + %d głosy + %d głosów + %d głosów + + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000..7dda2b8 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,730 @@ + + + Editar + Adicionar + Adicionar a Notas + Conversa %1$s adicionada para favoritos + Pesquisar em %s + Aparecer off-line + Arquivar conversa + Quando uma conversa for arquivada, ela ficará oculta por padrão. Selecione o filtro \"Arquivada\" para visualizar as conversas arquivadas. As menções diretas ainda serão recebidas. + Arquivada + %1$s arquivada + Chamada de áudio + Bluetooth + Saída de áudio + Telefone + Alto-falante + Fone de ouvido com fio + Seu status foi definido automaticamente + Avatar + Ausente + Botão voltar + Banir + Banir participante + Lista de banimentos + Ocupado + Calendário + Opções avançadas de chamada + A chamada está em andamento há uma hora. + Chamada sem notificação + Permissão da câmera concedida. Por favor, escolha a câmera novamente. + Cancelar Login + Escolher o avatar da nuvem + Limpar mensagem de status + Limpar mensagem de status após + Fechar + Ícone de Fechar + Conexão estabelecida + Sem conexão com o servidor + Conexão perdida - As mensagens enviadas são colocadas na fila + Fixar gravação para gravação contínua da mensagem de voz + Conversa é arquivada + A conversa é somente leitura + Falha ao definir a conversa Somente leitura + Conversas + Criar conversa + Relatar problema + Personalizado + Zona de perigo + %1$s em %2$s + Excluir avatar + Excluir gravação de voz + Conversa %1$s excluída + Não perturbe + Não limpe + Editar + Mensagens mais antigas do que 24 horas não podem ser editadas + Editar mensagem + Recentes + Criptografado + Encerrar chamada + Encerrar chamada para todos + Ocorreu um problema ao carregar seus bate-papos + Ocorreu um erro ao cancelar o banimento do participante + Falha ao salvar %1$s + 15 minutos + pasta + Carregando … + %1$s (%2$d) + Fios seguidos + 4 horas + Falha ao buscar convites pendentes + (editado) + Nota interna + Invisível + Não foi possível recuperar os idiomas + Falha na recuperação + Hoje mais tarde + Sair da chamada + Você saiu da conversa %1$s + Carregar mais resultados + Horário local: %1$s + Permissão de localização negada + Por favor, ative-a nas configurações do aplicativo + Serviços de localização desativados + Ative os serviços de localização (GPS) para usar este recurso + Bloquear conversa + Símbolo de cadeado + Baixar mão + Conversa %1$s marcada como lida + Conversa %1$s marcada como não lida + Mencionado + Mais novo primeiro + Mais antigo primeiro + A - Z + Z - A + Maior primeiro + Menor primeiro + Mensagem copiada + Tem certeza de que deseja excluir esta mensagem? + Mensagem excluída por você + Editado por %1$s + Toque para abrir enquete + Nenhum resultado encontrado + Comece a digitar para pesquisar … + Pesquisar … + Mensagens + Silenciar todas as notificações + A conta selecionada agora está importada e disponível + Sobre + Usuário ativo + Adicionar conta + A conta está agendada para exclusão e não pode ser alterada + Abrir menu principal + Adicionar anexo + Adicionar emojis + Adicionar à conversa + Adicionar participantes + Adicionar aos favoritos + OK, tudo pronto! + PIN: %1$s + Desbloquear %1$s + Para ativar os alto-falantes bluetooth, conceda a permissão \"Dispositivos próximos\". + Atender como chamada de vídeo + Atender apenas como chamada de voz + Mudar saída de áudio + Alternar a câmera + Desligar + Alternar o microfone + Abrir modo imagem-em-imagem + Mudar para próprio vídeo + ENTRADA + Nome de conversação + Notificações de chamadas + %1$s levantou a mão + Reconectando … + TOCANDO + %1$s em chamada + %1$s com telefone + %1$s com vídeo + 45 segundos sem resposta, toque para retentar + %s chamada + %s chamada de vídeo + %s chamada de voz + Para ativar a comunicação por vídeo, por favor, conceda a permissão \"Câmera\". + Cancelar + Falhou ao buscar recursos, interrompendo + Legenda + Quer confiar em um certificado SSL desconhecido, emitido por %1$s para %2$s, válido de %3$s a %4$s? + Confira o certificado + Sua configuração SSL impediu a conexão + Alterar certificado de autenticação + Alterar senha + Cancelar edição + Cancelar edição + Excluir todas as mensagens + Todas as mensagens foram excluídas + Tem certeza de que deseja excluir todas as mensagens desta conversa? + Alterar certificado do cliente + Configurar certificado do cliente + e + Copiar + Copiado para a área de transferência + Criar + Desativado + Dispensar + Desculpe, algo deu errado! + Mais opções + Definir + Pular + Desconhecido + Selecionar certificado de autenticação + Conectando … + Concluído + Descrição da conversa + Informação da conversa + Chamada de vídeo + Chamada de voz + Conversa não encontrada + Configurações da conversa + Entre em uma conversa ou inicie uma nova + Diga olá para seus amigos e colegas! + Copiar + Criar uma nova conversa + Criar enquete + Você: + Hoje + Ontem + Excluir + Excluir todos + Excluir conversa + Se você excluir a conversa, ela também será excluída para todos os outros participantes. + Excluir mensagem + Mensagem excluída com sucesso, mas pode ter sido vazada para outros serviços + Excluir agora + Usuário %1$s foi removido + Rebaixar de moderador + Gravar mensagem de voz + Enviar mensagem + Conta atual + Servidor + Aplicativo de notificação do servidor instalado? + Usuário + Status do usuário ativado? + Versão do Android + Aplicativo + Nome do aplicativo + Usuários registrados + Versão do aplicativo + A otimização da bateria é ignorada, tudo bem + A otimização da bateria está ativada, o que pode causar problemas. Você deve desativar a otimização da bateria! + Configurações da bateria + Dispositivo + Abrir lista de verificação de solução de problemas + Abrir tela de diagnóstico + Abrir dontkillmyapp.com + Última busca de token push do Firebase + Última geração de token push do Firebase + Nenhum token push do Firebase definido. Por favor, crie um relatório de bug. + Token push do Firebase + Os serviços do Google Play não estão disponíveis. Notificações não são suportadas + Serviços do Google Play + Os serviços do Google Play estão disponíveis + Último registro push no push proxy + Ainda não registrado no push proxy + Registro push mais recente no servidor + Ainda não registrado no servidor + Meta informações + Geração de relatório do sistema + Canal de notificação de chamadas ativado? + Canal de notificação de mensagens ativado? + Permissões de notificação + Telefone + Versão do servidor Talk + Versão do servidor + Externo + Interno + Modo de sinalização + Senha inválida + O servidor está atualmente em modo de manutenção. + Aplicativo está desatualizado + O aplicativo é muito antigo e não é mais suportado por este servidor. Por favor, atualize. + Atualizar + Quer reautorizar ou excluir esta conta? + Salvar esta mídia no armazenamento permitirá que qualquer outro aplicativo em seu dispositivo a acesse. + Continuar? + Não + Salvar no armazenamento? + Sim + Nome de exibição não obtido, interrompendo + Não foi possível armazenar o nome de exibição, cancelando + Editar + Editar + Editar mensagem + Editado por um administrador + Menu de conversa de evento + Agenda + 8 horas + 4 semanas + Desligado + 1 dia + 1 hora + 1 semana + Deixar as mensagens de bate-papo expirarem + As mensagens de bate-papo podem expirar após um certo tempo. Observação: os arquivos compartilhados no bate-papo não serão excluídos para o proprietário, mas não serão mais compartilhados na conversa. + Falha ao buscar configurações de sinalização + Aceitar + Rejeitar + de %1$s em %2$s + Nenhum convite pendente + Você tem convites pendentes + Voltar + É necessária permissão para acesso aos arquivos + Filtrar conversas + Usuário seguindo um link público + Você: %1$s + Encaminhar + Encaminhar para … + Galeria + Não tem servidor ainda?\nClique aqui para obter um + Obtenha o código-fonte + Grupo + Convidado + Acesso para convidados + Não é possível ativar/desativar o acesso para convidados. + Permitir que os convidados compartilhem um link público para participar desta conversa. + Permitir convidados + Insira uma senha + Senha de acesso para convidados + Erro durante a configuração/desativação da senha. + Definir uma senha para restringir quem pode usar o link público. + Proteção por senha + Reenviar convites + Os convites não foram enviados devido a um erro. + Os convites foram enviados novamente. + Compartilhar o link da conversa + Digite uma mensagem … + A otimização da bateria não é ignorada. Isso deve ser alterado para garantir que as notificações funcionem em segundo plano! Clique em OK e selecione \"Todos os apps\" -> %1$s -> Não otimizar + Ignorar otimização da bateria + Conversa importante + O status de usuário \"Não perturbe\" é ignorado em conversas importantes + Horário inválido + Convites + Entrar em conversas abertas + Manter + Você precisa promover um novo moderador antes de sair da conversa + %1$s | Modificado recentemente: %2$s + Sair da conversa + Saindo da chamada … + GNU General Public License, Versão 3 + Licença + O limite de %s caracteres foi atingido + Sala de Espera + Esta reunião está agendada para %1$s + A reunião vai começar em breve + No momento, você está esperando na sala de espera. + Sua localização atual + permissão de localização é necessária + Posição desconhecida + Bloqueado + Toque para desbloquear + Não definido + Marcar como lido + Marcar como não lido + Conversa marcada como importante + Conversa desmarcada como sensível + Conversa marcada como sensível + Conversa desmarcada como importante + Reunião encerrada + Mensagem adicionada às notas + Falhou + Falha ao enviar mensagem: + Off-line + Cancelar resposta + Mensagem lida + Enviando + Mensagem enviada + O microfone está ativado e o áudio está sendo gravado + Para ativar a comunicação por voz, conceda a permissão \"Microfone\". + Você perdeu uma chamada de %s + Moderador + Nova conversa + Visibilidade + Menções não lidas + Mensagens não lidas + %1$s não está disponível (não instalado ou restrito pelo administrador) + Convidado + Não + Nenhuma conversa aberta + Não há conversas abertas nas quais você possa entrar.\nOu não há conversas abertas ou você já entrou em todas elas. + Sem proxy + Você não tem permissão para ativar o áudio! + Você não tem permissão para ativar o vídeo! + Agora não + %1$s no canal de notificação %2$s + Chamadas + Notificar sobre chamadas recebidas + Mensagens + Notificar sobre mensagens recebidas + Uploads + Notificar sobre o progresso do upload + Configurações de notificação + As notificações não estão configuradas corretamente + A permissão de notificação e as configurações da bateria estão configuradas corretamente para receber notificações. Se você tiver problemas para receber notificações mesmo assim, verifique se os canais de notificação para chamadas e mensagens estão habilitados. Mais ajuda pode ser encontrada em DontKillMyApp.com ou na lista de verificação de solução de problemas. Se isso não ajudar, vá para a tela de diagnóstico e envie um relatório de bug. + Solução de problemas de notificação + Sempre notificar + Notificar quando mencionado + Nunca notificar + Off-line no momento, cheque sua conectividade + OK + Reunião em andamento + Abrir conversa a usuários registrados + Também abrir a usuários do aplicativo de convidados + Proprietário + Participantes + Adicionar participantes + Senha + Definir permissões + Algumas permissões foram negadas. + Por favor, permita permissões + Abrir as configurações + Por favor, conceda permissões em Configurações > Permissões + Conta não encontrada + Bate-papo via %s + Silenciar microfone + Ativar microfone + Mensagens + Privacidade + Informações Pessoais + Promover a moderador + Conversa pública + Notificações push desativadas + Desculpe, algo deu errado, o erro é %1$s + Desculpe, algo deu errado, não foi possível obter a mensagem push de teste + A notificação push foi enviada com êxito. Agora você deve receber uma notificação neste dispositivo com o título \'Testando notificações push\' + Pressione-para-falar + Com o microfone desativado, clique& para usar Pressione-para-falar + Lembre-me mais tarde + Remover dos favoritos + Remover grupo e membros + Excluir participante + Remover Senha + Remover equipe e membros + Renomear conversa + Renomear + Responder + Responder privadamente + A sala é mantida com sucesso + Salvar + Salvo com sucesso + 30 segundos + 5 minutos + 1 minuto + 10 minutos + Imediato + 600 + 60 + 30 + 300 + Pesquisar + Limpar pesquisa + Selecionar uma conta + Atualizar mensagem + Enviar gravação de voz + Conversa sensível + A visualização de mensagens será desativada na lista de conversas e nas notificações + %1$s enviou um GIF. + Você enviou um GIF. + %1$s enviou um vídeo. + Você enviou um vídeo. + %1$s enviou um áudio. + Você enviou um áudio. + %1$s enviou uma imagem. + Você enviou uma imagem. + %1$s enviou um cartão de Deck + Testar a conexão do servidor + Atualize seu banco de dados %1$s + Falha ao importar a conta + O link da interface da web do seu %1$s quando você o abre no navegador. + Importar conta do aplicativo %1$s + Importar conta + Importar contas do aplicativo %1$s + Importar contas + Coloque sua %1$s em manutenção + Termine sua instalação %1$s + Testando conexão + O servidor não tem um aplicativo Talk suportado instalado + Endereço do servidor https://… + %1$s somente funciona com%2$s 13 e posterior + Definir nova senha + Definir Senha + Configurações + Sua conta atual foi atualizada, em vez de adicionar uma nova + Avançadas + Aparência + Chamadas + Por favor, entre em contato com o administrador de + Abrir tela de diagnóstico para verificar as configurações ou criar um relatório de bug + Diagnóstico + Instrui o teclado para desativar o aprendizado personalizado (sem garantias) + Teclado incógnito + Sem som + O aplicativo Talk não está instalado no servidor em que você tentou se autenticar + Notificações + As notificações foram recusadas + As notificações são concedidas + Mensagens + Combinar os contatos com base no número de telefone para integrar o atalho do Talk no aplicativo de contatos do sistema + Erro 429 Pedidos Em Excesso + Você pode definir seu número de telefone para que outros usuários possam encontrar você + Digite o número de telefone + Número de telefone inválido + Número de telefone definido com sucesso + Número do telefone + Integração de número de telefone + Privacidade + Endereço do proxy + Senha do proxy + Porta do proxy + Tipo de proxy + Nome de usuário do proxy + Compartilhar meu status de leitura e mostrá-los a outros + Status de leitura + Reautorizar conta + Excluir + Excluir conta + Confirme se quer realmente excluir a conta atual. + Bloquear %1$s com o bloqueio de tela do Android ou um método biométrico suportado + Tempo limite para bloqueio de tela + Bloquear tela + Impede capturas de tela na lista recente e dentro do aplicativo + Segurança da tela + A versão do servidor é muito antiga e não será suportada na próxima versão! + A versão do servidor é muito antiga e não suporta esta versão do aplicativo para Android + Servidor não suportado + Aplicativo de notificações do servidor não instalado + Definido pelo economizador de bateria + Escuro + Use o padrão do sistema + tema + Claro + Tema + Compartilhar meu status de digitação e exibir status de digitação dos outros + Status de digitação só está disponível ao usar um back-end de alto desempenho (HPB - high performance backend) + Status de digitação + Proxy requer credenciais + Aviso + Somente a conta atual pode ser reautorizada + Compartilhar contato + Permissão para ler os contatos é necessária + Compartilhar localização atual + Compartilhar link + Compartilhar localização + Compartilhar esta localização + Escolha uma conta + Itens compartilhados + Cartão de Deck + Imagens, arquivos, mensagens de voz … + Nenhum item compartilhado + Localização + Localização compartilhada + Quando as notificações não estiverem configuradas corretamente, mostre um aviso regular + Mostrar aviso de notificação regular + Ordenar por + Iniciar bate-papo em grupo + Hora de início + Mudar de conta + Equipe + Testar notificações push + Resultados do teste + Hoje às %1$s + Amanhã às %1$s + Escolher arquivos + Enviar estes arquivos para %1$s? + Enviar este arquivo para %1$s? + Desculpe, falha no upload + Falha no upload de %1$s + Falha + Compartilhamento de %1$s + Fazer upload do dispositivo + Fazendo upload + %1$s para %2$s - %3$s\%% + Tirar foto + Gravar vídeo + Usuário + Gravação de vídeo de %1$s + Gravação de conversa de %1$s (%2$s) + Segure para gravar, solte para enviar. + A permissão para gravação de áudio é necessária + « Deslize para cancelar + Webinarário + Sim + Próxima semana + Nenhuma conversa arquivada + Nenhuma mensagem off-line foi salva + Sem integração de número de telefone devido à falta de permissões + Todas as mensagens + Apenas menções de @ + Desativado + Padrão + Seguir as configurações da conversa + 1 hora + On-line + Status on-line + Conversas abertas + Abrir no aplicativo Arquivos + Abrir Notas + Ir para fio + Reproduzir/pausar mensagem de voz + Controle de velocidade de reprodução + Adicionar opção + Editar voto + Encerrar enquete + Você realmente quer encerrar esta enquete? Isto não pode ser desfeito. + Você não pode votar com mais opções para esta enquete. + Várias respostas + Excluir opção %1$d + Opção %1$d + Opções + Enquete privada + Pergunta + Sua pergunta + Resultados + Configurações + Votar + Voto enviado + Definido anteriormente + Não foi possível ler o código QR + Levantar mão + Todos + O compartilhamento de arquivos do armazenamento não é possível sem permissões + Fios recentes + A chamada está sendo gravada + Cancelar início da gravação + A gravação falhou. Entre em contato com o administrador. + Iniciar gravação + Você realmente deseja encerrar a gravação? + Encerrar gravação de chamada + Encerrar gravação + Parando a gravação … + O consentimento de gravação é necessário para todas as chamadas + A gravação pode incluir sua voz, vídeo da câmera e compartilhamento de tela. Seu consentimento é obrigatório antes de entrar na chamada. Você consente? + Exigir consentimento de gravação antes de entrar em uma chamada nesta conversa + Consentimento de gravação + A chamada pode ser gravada. + Gravação + Conversa %1$s removida dos favoritos + Conversação %1$s foi renomeada + Reenviar + Redefinir status + Não é possível entrar em outras salas enquanto estiver em uma chamada + Salvar + Digitalizar Código QR + Sincronizar apenas com servidores confiáveis + Federado + Visível apenas para pessoas nesta instância e convidados + Local + Visível apenas para as pessoas correspondentes por meio da integração do número de telefone pelo Talk no celular + Privado + Sincronizar com servidores confiáveis ​​e com o catálogo de endereços público e global + Publicado + Alternância de escopo + Alterar o nível de privacidade de %1$s + Rolar até o final + Ícone de Pesquisa + segundos atrás + Selecionado + Enviar e-mail + Enviar para + Você não tem permissão para compartilhar conteúdo neste bate-papo + Enviar para … + Enviar sem notificação + Definir + Definir avatar a partir da câmera + Definir status + Definir mensagem de status + Compartilhar + Entrar na conversa %1$s em %2$s + Áudio + Arquivo + Mídia + Outro + Enquete + Gravação de chamada + Voz + Mostrar motivo do banimento + Mostrar participantes banidos + Favorito + Você não tem permissão para iniciar uma chamada + Criar um fio + iniciou uma chamada + Mensagem de status + Status Revertido + Mudar para a sala temática + Mudar para a sala principal + Tirar uma foto + Erro ao tirar foto + Tirar uma foto não é possível sem permissões + Tirar a foto novamente + Enviar + Trocar câmera + Cortar foto + Reduzir tamanho da imagem + Alternar a lanterna + 30 minutos + Esta semana + Esta é uma mensagem de teste + Este fim de semana + Cancelar a criação do fio + Notificações de fios + Responder + Título do fio + Hoje + Amanhã + Traduzir + Tradução + Copiar texto traduzido + Detectar idioma + Configurações do dispositivo + Não foi possível detectar o idioma + Falha na tradução + De + Para + e 1 outro está digitando … + estão digitando … + está digitando … + e %1$s outros estão digitando … + Desarquivar conversa + Quando uma conversa for desarquivada, ela será mostrada novamente por padrão. + %1$s desarquivado + Desbanir + Não lido + Carregar novo avatar do dispositivo + %1$s está fora do escritório e pode não responder + %1$s está fora do escritório hoje + Substituto: + Avatar do usuário + Endereço + Nome completo + E-mail + Número do telefone + Twitter + Website + Status + Falha ao recuperar informações pessoais do usuário. + Nenhumas informações pessoais definidas + Adicione nome, foto e detalhes de contato em sua página de perfil. + Videochamada + Qual é o seu status? + + Veja %d mensagem semelhante + Veja %d mensagens semelhantes + Veja %d mensagens semelhantes + + + Esta conversa será excluída automaticamente para todos em %1$d dia sem atividade + Esta conversa será excluída automaticamente para todos em %1$d de dias sem atividade + Esta conversa será excluída automaticamente para todos em %1$d dias sem atividade + + + %d resposta + %d de respostas + %d respostas + + + %d voto + %d votos + %d votos + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000..c72b8b1 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,722 @@ + + + Редактирование + Добавить + Добавить в заметки + Обсуждение добавлено %1$s в избранное + Искать в %s + \"Не в сети\" для остальных + Архивировать обсуждение + После архивации обсуждение будет скрыто по-умолчанию. Выберите фильтр \"Архивировано\", чтобы увидеть архивированные обсуждения. Прямые упоминания все еще будут получены. + В архиве + Архивировано %1$s + Голосовой звонок + Bluetooth + Аудиовыход + Телефон + Динамик + Проводная гарнитура + Ваш статус был установлен автоматически + Аватар + Отошёл + Кнопка Назад + Заблокировать + Заблокировать участника + Список заблокированных + Занят + Календарь + Дополнительные настройки звонка + Звонок длится уже час + Звонок без уведомления + Разрешение на камеру получено. Пожалуйста, выберите камеру снова. + Отменить вход + Выберите аватар из облака + Очистить сообщение о статусе + Очищать статус после + Закрыть + Значок Закрыть + Соединение установлено + Нет подключения к серверу + Соединение потеряно - отправленные сообщения ставятся в очередь + Блокировка записи для непрерывной записи голосового сообщения + Обсуждение архивировано + Разговор доступен только для чтения + Ошибка установки обсуждения только на чтение + Беседы + Создать беседу + Создать запрос + Задать + Опасная зона + %1$s в %2$s + Удалить аватар + Удалить голосовую запись + Удалённый чат %1$s + Не беспокоить + Не очищать + Редактировать + Сообщения старше 24 часов нельзя редактировать + Редактировать сообщение + Недавние + Зашифровано + Завершить вызов + Завершить звонок для всех + Возникла проблема при загрузке ваших чатов + Произошла ошибка при разблокировке участника + Не удалось сохранить %1$s + 15 минут + каталог + Загрузка … + %1$s (%2$d) + Отслеживаемые темы + 4 часа + Не удалось получить ожидающие приглашения + (изменено) + Внутреннее примечание + Невидимый + Языки не могут быть извлечены + Не удалось получить + Позже сегодня + Покинуть вызов + Вы покинули чат %1$s + Показать больше результатов + Местное время: %1$s + Заблокировать обсуждение + Символ блокировки + Опустить руку + Отметить чат %1$s как прочитанный + Отметить чат %1$s как непрочитанный + Упомянул + Самый новый первый + Самый старый первый + А - Я + Я - А + Самый большой первый + Самый маленький первый + Сообщение скопировано + Вы уверены, что хотите удалить это сообщение? + Сообщение удалено вами + Изменено %1$s + Нажмите для открытия опроса + Ничего не найдено + Начните печатать для поиска … + Поиск … + Сообщения + Без уведомлений + Выбранная учётная запись теперь доступна + О программе + Активный пользователь + Добавить аккаунт + Учётная запись запланирована к удалению и не может быть использована + Открыть главное меню + Добавить вложение + Добавить эмодзи + Добавить к разговору + Добавить участников + Добавить в избранное + Всё готово! + Пин: %1$s + Разблокировать %1$s + Чтобы включить динамики Bluetooth, предоставьте разрешение «Устройства поблизости». + Ответить как видеозвонок + Ответить как голосовой звонок + Изменить аудио-выход + Включить/выключить камеру + Повесить трубку + Включить/выключить микрофон + Открыть в режиме \"картинка в картинке\" + Переключить видео на себя + Входящий + Наименование беседы + Уведомления о звонках + %1$s поднял руку + Переподключение … + Звонок + %1$s на линии + %1$s с телефоном + %1$s с видео + Нет ответа в течение 45 секунд, нажмите, чтобы повторить попытку + %s звонок + %s видеозвонок + %s голосовой вызов + Для включения видеосвязи предоставьте разрешение «Камера». + Отмена + Не удалось получить список поддерживаемых протоколов + Подпись + Доверять неизвестному сертификату SSL: выпущен %1$s для %2$s, действителен с %3$s по %4$s? + Проверьте сертификат + Заданные параметры SSL не позволяют подключиться + Изменить сертификат для подтверждения подлинности + Изменить пароль + Отменить редактирование + Отменить редактирование + Удалить все сообщения + Все сообщения были удалены + Вы действительно хотите удалить все сообщения в этой беседе? + Изменение клиентского сертификата + Установка клиентского сертификата + и + Копировать + Скопировано в буфер обмена + Создать + Отключено + Отклонить + Что-то пошло не так :( + Дополнительные параметры + Установить + Пропустить + Неизвестно + Выбрать сертификат для подтверждения подлинности + Соединение… + Готово + Описание беседы + Информация о беседе + Видеозвонок + Голосовой вызов + Разговор не найден + Параметры обсуждения + Присоединитесь к обсуждению или же начните новое + Поприветствуйте своих друзей и коллег. + Копировать + Создать новое обсуждение + Создать опрос + Вы: + Сегодня + Вчера + Удалить + Удалить все + Удалить беседу + При удалении беседы, она будет также удалена у других участников + Удалить сообщение + Сообщение удалено, но его содержимое могло быть прочитано другими службами + Удалить сейчас + Пользователь %1$s был удален + Исключить из модераторов + Запись голосового сообщения + Отправить сообщение + Текущая учётная запись + Сервер + Установлено ли приложение для уведомлений сервера? + Пользователь + Статус пользователя включен? + Версия ОС Android + Приложение + Название приложения + Зарегистрированные пользователи + Версия приложения + Оптимизация батареи игнорируется, все в порядке + Включена оптимизация батареи, что может вызвать проблемы. Вам нужно отключить оптимизацию батареи! + Настройки батареи + Устройство + Открыть контрольный список устранения неполадок + Открыть экран диагностики + Открыть dontkillmyapp.com + Получение последнего пуш-токена firebaseпоследнийпоследний + Генерация последнего пуш-токена firebase + Не установлен пуш-токен firebase. Создайте сообщение о проблеме. + Пуш-токен firebase + Сервисы Google Play недоступны. Уведомления не поддерживаются + Сервисы Google Play + Сервисы Google Play доступны + Последняя пуш-регистрация на пуш-прокси + Пока не зарегистрирован на пуш-прокси + Последняя пуш-регистрация на сервере + Пока не зарегистрирован на сервере + Общее уведомление + Генерация отчета системы + Включен ли канал голосовых звонков? + Включен ли канал уведомлений? + Разрешения на уведомления + Телефон + Версия Talk на сервере + Версия сервера + Внешний + Внутренние + Режим сигнализации + Неверный пароль + В настоящее время сервер находится в режиме технического обслуживания. + Приложение устарело + Версия клиента слишком старая и больше не поддерживается этим сервером. Пожалуйста обновите его. + Обновить + Вы хотите повторно авторизоваться или удалить эту учётную запись? + Сохранение этого медиафайла в хранилище позволит всем другим приложениям на вашем устройстве получить к нему доступ. + Продолжить? + Нет + Сохранить в хранилище? + Да + Не удалось получить отображаемое имя, продолжение невозможно + Не удалось сохранить отображаемое имя, продолжение невозможно + Редактирование + Редактирование + Редактировать сообщение + Изменено администратором + Меню обсуждения события + Расписание + 8 часов + 4 недели + Отключить + 1 день + 1 час + 1 неделя + Истечение срока действия сообщений в чате + Сообщения чата могут быть удалены по истечении определенного времени. Примечание: Файлы, переданные в чате, не будут удалены для владельца, но больше не будут доступны в беседе. + Не удалось получить параметры сигнализации + Принять + Отклонить + из %1$s на %2$s + Нет ожидающих приглашений + У вас есть ожидающие приглашения + Назад + Требуется разрешение на доступ к файлу + Фильтровать обсуждения + Пользователь, вошедший по ссылке + Вы: %1$s + Переслать + Вперёд к … + Галерея + Нет своего сервера?\nНажмите здесь чтобы заказать у провайдера + Исходный код + Группа + Гость + Гостевой доступ + Невозможно включить/выключить гостевой доступ. + Разрешите гостям делиться публичной ссылкой, чтобы присоединиться к этому разговору. + Разрешить гостей + Введите пароль + Пароль гостевого доступа + Ошибка при установке/отключении пароля. + Задать пароль для входа по открытой ссылке. + Защита паролем + Повторная отправка приглашений + Приглашения не были отправлены из-за ошибки. + Приглашения были разосланы повторно. + Поделиться ссылкой на беседу + Напишите сообщение … + Оптимизация батареи не игнорируется. Это необходимо изменить, чтобы уведомления работали в фоне. Нажмите OK и выберите \"Все приложения\" -> %1$s -> Не оптимизировать + Игнорировать оптимизацию батареи + Важное обсуждение + Статус «Не беспокоить» игнорируется для важных обсуждений + Некорректное время + Приглашения + Присоединиться к открытым обсуждениям + Сохранить + Вам нужно назначить нового модератора перед тем, как покинуть обсуждение + %1$s | Последнее изменение: %2$s + Покинуть беседу + Завершение вызова … + Открытое лицензионное соглашение GNU, версия 3 + Лицензия + Достигнуто ограничение в %s знаков + Лобби + Эта встреча запланирована на %1$s + Встреча скоро начнётся + В данный момент Вы ожидаете в вестибюле. + Ваше текущее местоположение + требуется разрешение на размещение + Положение неизвестно + Заблокировано + Коснитесь для разблокирования + Не задано + Отметить прочитанным + Отметить непрочитанным + Обсуждение помечено как важное + Обсуждение не помечено как конфиденциальное + Обсуждение помечено как конфиденциальное + Обсуждение не помечено как важное + Встреча завершена + Сообщение добавлено в записки + Не удалось + Не удалось отправить сообщение: + Не в сети + Отменить ответ + Сообщение прочитано + Отправка + Сообщение отправлено + Микрофон включен, и беседа записывается + Для включения голосовой связи предоставьте разрешение «Микрофон». + Вы пропустили звонок от %s + Модератор + Новый чат + Видимость + Непрочитанные упоминания + Непрочитанные сообщения + Приложение %1$s недоступно (не установлено, либо использование приложения ограничено администратором) + Гость + Нет + Нет открытых чатов + Нет открытых обсуждений, к которым вы могли бы присоединиться. Либо открытых обсуждений нет, либо вы уже подключились ко всем. + Не использовать прокси-сервер + Вам запрещено активировать аудио! + Вам запрещено активировать видео! + Не сейчас + канал уведомлений %1$s на %2$s + Звонки + Уведомление о входящих вызовах + Сообщения + Уведомления о входящих сообщениях + Загрузки + Уведомление о ходе загрузки + Параметры уведомлений + Уведомления некорректно настроены + Разрешение на уведомления и настройки батареи настроены правильно для получения уведомлений. Если у вас всё равно возникли проблемы с получением уведомлений, проверьте, включены ли каналы уведомлений для звонков и сообщений. Дополнительную помощь можно найти на сайте DontKillMyApp.com или в контрольном списке устранения неполадок. Если это не помогло, перейдите на экран диагностики и отправьте отчёт об ошибке. + Устранение неполадок с уведомлениями + Всегда уведомлять + Уведомлять при упоминании + Не уведомлять + Нет подключения к сети, пожалуйста, проверьте ваше соединение + ОК + Идущая беседа + Открыть обсуждение для зарегистрированных пользователей + Открыть для гостей + Владелец + Участники + Добавить участников + Пароль + Установить разрешения + В некоторых разрешениях было отказано. + Пожалуйста, разрешить права доступа + Открыть настройки + Пожалуйста, предоставьте разрешения в разделе «Настройки» > «Разрешения». + Учётная запись не найдена + Чат через %s + Выключить микрофон + Включить микрофон + Сообщения + Конфиденциальность + Личная информация + Назначить модератором + Открытое обсуждение + Всплывающие уведомления отключены + Что-то пошло не так, ошибка %1$s + Что-то пошло не так, не могу получить тестовое пуш-сообщение + Пуш-сообщение отправлено успешно. Вы теперь должны получить сообщение на этом устройстве с заголовком \"Тестирование пуш-сообщений\" + PTT (нажми чтобы говорить) + Нажмите и удерживайте для использования PTT при отключённом микрофоне + Напомнить позже + Удалить из избранного + Удалить группу и её участников + Удалить участника + Удалить пароль + Исключить команду и её участников + Переименовать разговор + Переименовать + Ответить + Ответить личным сообщением + Комната сохранена успешно + Сохранить + Сохранено успешно + 30 секунд + 5 минут + 1 минута + 10 минут + Немедленно + 600 + 60 + 30 + 300 + Поиск + Очистить поиск + Выберите учётную запись + Обновить сообщение + Послать голосовую запись + Конфиденциальное обсуждение + Предварительный просмотр сообщений будет отключен в списке обсуждений и уведомлениях + %1$s отправил(а) GIF. + Вы отправили GIF. + %1$s отправил(а) файл видео. + Вы отправили файл видео. + %1$s отправил(а) аудиофайл. + Вы отправили аудиофайл. + %1$s отправил(а) изображение. + Вы отправили изображение. + %1$s отправил карточку + Проверить соединение с сервером + Обновите базу данных %1$s + Не удалось импортировать выбранную учётную запись + Ссылка на ваш %1$s веб-интерфейс, когда вы открываете его в браузере. + Импортировать учётную запись из приложения %1$s + Импортировать учётную запись + Импортировать учётные записи из приложения %1$s + Импортировать учётные записи + Сервер %1$s находится в режиме обслуживания + Завершите установку %1$s + Проверка соединения + На сервере не установлено приложение «Конференции» + Адрес сервера https://… + Работа приложения %1$s возможна только с серверами %2$s версии 13 или старше + Установить новый пароль + Установить пароль + Параметры + Вместо создания новой учётной записи было выполнено обновление существующей + Дополнительно + Внешний вид + Вызовы + Свяжитесь с администратором + Откройте экран диагностики, чтобы проверить настройки или создать отчёт об ошибке. + Диагностика + Указывает клавиатуре отключить персонализированное обучение (без гарантий) + Клавиатура инкогнито + Без звука + На сервере, к которому вы пытаетесь подключиться, приложение «Talk» не установлено. + Уведомления + Уведомления отклонены + Уведомления отклонены. Разрешите уведомления в настройках Android + Сообщения + Сопоставление контактов по номеру телефона для создания ярлыков на приложения «Конференции» в приложение системных контактов + Ошибка 429 Слишком много запросов + Вы можете указать свой номер телефона, чтобы другие пользователи могли вас найти + Введите номер телефона + Неправильный номер телефона + Номер телефона успешно установлен + Номер телефона + Интеграция номера телефона + Приватность + Узел прокси-сервера + Пароль прокси-сервера + Порт прокси-сервера + Тип прокси-сервера + Имя пользователя прокси-сервера + Просматривать и показывать отчёт о прочтении сообщений + Отчёт о прочтении + Повторная авторизация учётной записи + Удалить + Удалить учётную запись + Пожалуйста, подтвердите удаление текущего аккаунта. + Защищать %1$s с помощью системой блокировки Android или поддерживаемого биометрического метода + Задержка блокирования экрана при бездействии + Блокировка экрана + Предотвращает сохранение скриншотов внутри приложения и в списке недавних приложений + Безопасность экрана + Версия сервера очень старая и не будет поддерживаться в следующем релизе! + Версия сервера слишком старая и не поддерживается данной версией приложения Android + Неподдерживаемый сервер + Приложение сообщений от сервера не установлно + Установлено охранителем батареи + Тёмное + Как в системе + стиль оформления + Светлое + Оформление + Поделитесь своим статусом набора текста и покажите статус набора текста другим + Статус печатания доступен только при использовании высокопроизводительного бэкенда (HPB) + Статус печатания + Прокси-серверу необходимы права доступа + Предупреждение + Повторный вход может быть выполнен только в текущую учётную запись + Поделиться контактом + Требуется разрешение на чтение контактов + Поделиться текущим местоположением + Поделиться ссылкой + Поделиться местоположением + Поделиться этим местом + Выбрать учётную запись + Общие элементы + Карточка + Изображения, файлы, голосовые сообщения … + Нет общих элементов + Местоположение + Местоположением поделились + Если уведомления настроены неверно, показывать регулярное предупреждение + Показывать регулярное предупреждение + Сортировать по + Начать групповой чат + Время начала + Сменить аккаунт + Команда + Проверить пуш-уведомления + Результаты теста + Сегодня в %1$s + Завтра в %1$s + Выберите файлы + Отправить эти файлы %1$s? + Отправить этот файл %1$s? + Ошибка передачи на сервер + Не удалось загрузить %1$s + Ошибка + Поделиться с %1$s + Загрузить с устройства + Выгружается + %1$s к %2$s - %3$s\%% + Сфотографировать + Снять видео + Пользователь + Запись видео с %1$s + Запись разговора с %1$s (%2$s) + Удерживайте для записи, отпустите для отправки. + Требуется разрешение на аудиозапись + « Сдвинуть для отмены + Вебинар + Да + Следующая неделя + Отсутствуют архивированные обсуждения + Нет сохраненных оффлайн сообщений + Отсутствуют разрешения на интеграцию номера телефона + Отключить + По умолчанию + Следовать настройкам обсуждения + 1 час + В сети + Онлайн статус + Открыть разговоры + Открыть в приложении Файлы + Открыть примечания + Перейти в тему + Воспроизведение/пауза голосового сообщения + Управление скоростью вопроизведения + Добавить вариант + Изменить голос + Завершить опрос + Вы действительно хотите закончить этот опрос? Это нельзя отменить. + Вы не можете голосовать с несколькими вариантами для этого опроса. + Множество ответов + Удалите опцию %1$d + Опция %1$d + Варианты + Закрытый опрос + Вопрос + Ваш вопрос + Результаты + Настройки + Голосовать + Голос отправлен + Установлено ранее + QR код не может быть прочитан + Поднять руку + Все + Общий доступ к файлам из хранилища невозможен без разрешений + Недавние темы + Данный звонок записывается + Отменить начало записи + Не удалось записать звонок. Пожалуйста, свяжитесь с администратором. + Начать запись + Вы действительно хотите остановить запись? + Остановить запись звонка + Остановить запись + Запись останавливается... + Согласие на запись вызова требуется для всех вызовов + В записи могут оказаться ваш голос, видео с камеры и изображения экрана. Ваше согласие необходимо перед присоединением ко звонку. Вы согласны? + Требовать согласие на запись вызова перед присоединением к вызову в этом обсуждении + Согласие на запись вызова + Звонок может быть записан. + Запись + Удалено обсуждение %1$s из избранного + Обсуждение %1$s переименовано + Отправить заново + Сбросить статус + Невозможно присоединиться к другим комнатам находясь в звонке + Сохранить + Сканировать QR код + Выполнять синхронизацию только с доверенными серверами + Федеративный доступ + Видно только людям на этом сервере и гостям + Локально + Видно только людям, сопоставленным с помощью интеграции номера телефона через приложение «Конференции» на мобильном телефоне + Закрытый + Выполнять синхронизацию с доверенными серверами и глобальной и открытой адресной книгой + Опубликовано + Переключить область видимости + Изменить уровень конфиденциальности %1$s + Прокрутить вниз + Поиск значка + несколько секунд назад + Выбрано + Отправить письмо + Отправить в + Вам не разрешено делиться контентом в этом чате + Отправить … + Отправить без уведомления + Указать + Установить аватар со снимка камерой + Установить статус + Установить статус + Поделиться + Вступить в обсуждение %1$s в %2$s + Звук + Файл + Медиа + Другое + Опрос + Запись звонка + Голосовая почта + Показать причину блокировки + Показать заблокированных участников + Избранное + Вам не разрешено звонить + Создать тему + начал(-а) звонок + Описание статуса + Статус возвращен + Перейти в комнат отдыха + Перейти в основную комнату + Сфотографировать + Ошибка при фотосъёмке + Фотосъёмка невозможна без разрешения + Переснять + Отправить + Переключить камеру + Обрезать фотографию + Уменьшить размер изображения + Переключатель фонарика + 30 минут + Эта неделя + Это тестовое сообщение + Эти выходные + Отменить создание темы + Уведомления для темы + Ответ + Заголовок темы + Сегодня + Завтра + Помочь с переводом + Перевод + Копировать переведенный текст + Определить язык + Параметры устройства + Не удалось определить язык + Не удалось перевести + От + Кому + и ещё 1 собеседник печатают... + печатают... + печатает... + и ещё %1$s собеседника печатают... + Разархивировать обсуждение + После разархивирования обсуждения оно будет видимо по-умолчанию + Разархивировано %1$s + Разбанить + Непрочитанное + Загрузить новый аватар с устройства + %1$s находится вне офиса и может не ответить + %1$s вне офиса сегодня + Замена: + Изображение профиля + Адрес + Полное имя + Эл. почта + Номер телефона + Twitter + Сайт + Состояние + Не удалось получить личную информации о пользователе + Личная информация не указана + На странице профиля укажите своё имя, добавьте изображение и подробные сведения + Видеозвонок + Какой у вас статус? + + Посмотреть %d похожее сообщение + Посмотреть %d похожих сообщения + Посмотреть %d похожих сообщений + Посмотреть %d похожих сообщений + + + Данное обсуждение будет автоматически удалено для всех через %1$d день отсутствия активности. + Данное обсуждение будет автоматически удалено для всех через %1$d дня отсутствия активности. + Данное обсуждение будет автоматически удалено для всех через %1$d дней отсутствия активности. + Данное обсуждение будет автоматически удалено для всех через %1$d дней отсутствия активности. + + + %d голос + %d голоса + %d голосов + %d голосов + + diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml new file mode 100644 index 0000000..04701b3 --- /dev/null +++ b/app/src/main/res/values-sc/strings.xml @@ -0,0 +1,403 @@ + + + Modìfica + Agiunghe + Chirca in %s + Mustra•ti foras de lìnia + Archiviadu + Telèfonu + Altoparlante + Avatar + Ausente + Impinnadu + Calendàriu + Sèbera s\'avatar dae sa nue virtuale + Lìmpia su messàgiu de istadu + Lìmpia su messàgiu de istadu a pustis + Serra + Connessione istabilida + Cunversatziones + Crea resonada + Personaliza + Zona de perìgulu + Cantzella s\'avatar + No istorbes + Modìfica + Reghente + Tzifradu + No at fatu a sarvare %1$s + 15 minutos + cartella + Carrigamentu … + %1$s (%2$d) + 4 oras + Invisìbile + Lassa sa mutida + Càrriga àteros resurtados + Bloca resonada + Sìmbolu de blocu + Bàscia sa manu + In antis is prus reghentes + In antis is prus betzos + A - Z + Z - A + Prima su prus mannu + Prima su prus piticu + Messàgiu cantzelladu dae tue + Perunu resurtadu de chirca + Messàgios + Istuda totu is notìficas + Su contu seletzionadu est importadu e a disponimentu + In contu de + Utèntzia ativa + Agiunghe contu + Su contu at a èssere cantzelladu e non si podet cambiare + Aberi su menu printzipale + Agiunghe alligongiadu + Agiunghe carigheddas + Agiunghe a sa resonada + Aguinghe partetzipantes + Agiunghe a preferidos + AB, fatu! + Apunta: %1$s + Isbloca %1$s + Serra + IN INTRADA + Nùmene resonada + SONENDE + %1$s in mutida + %1$s cun telèfonu + %1$s cun vìdeu + Peruna risposta in 45 segundos, toca pro torrare a proare + %s tzèrria + %s mutida de vìdeu + %s mutida de boghe + Annulla + Errore in su recùperu de is capatzidades, annullende + Ti fidas de su tzertificadu SSL, disconnotu finas a immoe, frunidu dae %1$s pro %2$s, vàlidu dae %3$s a %4$s? + Controlla su tzertificadu + Sa cunfiguratzione SSL at refudadu sa connessione + Càmbia tzertificadu autenticatzione + Càmbia crae + Cantzella totu is messàgios + Cantzellados totu is messàgios + A beru boles cantzellare totu is messàgios in custa resonada? + Càmbia tzertificadu cliente + Cunfigura su tzertificadu de cliente + e + Còpia + Copiadu in punta de billete + Crea + Disativadu + Iscarta + Ddoe at àpidu un\'errore! + Cunfigura + Brinca + Disconnotu + Seletziona tzertificadu autenticatzione + Connetende … + Fatu + Informatziones de resonada + Mutida de vìdeu + Mutida de boghe + Resonada no agatada + Cunfiguratzione de sa resonada + Intra in una resonada o cumintza•nde una noa + Saluda a sa gente! + Còpia + Crea una resonada noa + Oe + Eris + Cantzella + Cantzella totu + Cantzella resonada + Si cantzellas sa resonada, s\'at a cantzellare puru pro su restu de partetzipantes. + Cantzella messàgiu + Messàgiu cantzelladu, ma diat pòdere èssere istadu dispensadu a àteros servìtzios + Lea·ssi su permissu de moderare + Registra messàgiu de boghe + Imbia messàgiu + Contu atuale + Serbidore + Utente + Versione Android + Aplicatzione + Nùmene de s\'aplicatzione + Cunfiguratzione de sa bateria + Dispositivu + Telèfonu + De foras + Internu + Crae non bàlida + Agiorna + Boles torrare a autorizare o cantzellare custu contu? + No + Si + Errore in su recùperu de su nùmene ammustradu, annullende + No at fatu a sarvare su nùmene ammustradu, annullende + Modìfica + Modìfica + Istudadu + 1 die + 1 ora + 1 chida + Errore in su recùperu de sa cunfiguratzione + Atzeta + Refuda + In segus + Utente chi sighit unu ligòngiu pùblicu + Tue: %1$s + Torra a imbiare + Torra a imbiare a … + Non tenes ancora unu serbidore?\nIncarca inoghe pro dd\'otènnere dae unu frunidore. + Otene su còdighe fonte + Grupu + Persone invitada + Autoriza persones invitadas + Dìgita una crae noa + Cunfigura una crae pro limitare is chi podent impreare su ligòngiu pùblicu. + Bardiadura de sa crae + Torra a imbiare is invitos + Cumpartzi su ligòngiu de sa resonada + Tzarrada importante + Mantene + %1$s | Ùrtima modìfica: %2$s + Lassa resonada + GNU General Public License, Versione 3 + Lissèntzia + %s lìmite de caràteres lòmpidu + Intrada + Immoe ses abetende in s\'intrada + Sa positzione tua atuale + su permissu de positzione est rechertu + Positzione disconnota + Blocadu + Toca pro isblocare + Non cunfiguradu + Marca comente lèghidu + Marca comente non lèghidu + Annulla risposta + Messàgiu lèghidu + Messàgiu imbiadu + Chie moderat + Resonada noa + Messàgios non lèghidos + Persone invitada + No + Perunu proxy + No immoe + %1$s in %2$s canale de notìfica + Mutidas + Messàgios + Carrigamentos + Cunfiguratzione de notìficas + Notificare semper + Notificare cando mentovadu + Non notificare mai + Foras de lìnia, controlla sa connessione tua + AB + Aberre sa resonada a is utèntzias registradas + Aberre puru a is utèntzias invitadas de s\'aplicatzione + Mere + Partetzipantes + Aguinghe partetzipantes + Crae + Aberi sa cunfiguratzione + Contu no agatadu + Tzarrada via %s + Messàgios + Riservadesa + Informatziones personales + Approva a moderadore + Notìficas push disativadas + Cumandu de trasmissione + Cun su micròfonu disativadu, mantene incarcadu&pro impreare su cumandu de trasmissione + Regorda•mi•ddu a coa + Boga dae preferidos + Boga·nche grupu e partetzipantes + Boga·nche partetzipante + Torra a numenare sa resonada + Risponde + Risponde in privadu + Sarva + Sarvadu + 30 segundos + 5 minutos + 1 minutu + 10 minutos + 600 + 60 + 30 + 300 + Chirca + Seletziona unu contu + %1$s at imbiadu una GIF. + As imbiadu una GIF. + %1$s at imbiadu unu vìdeu. + As imbiadu unu vìdeu. + %1$s at imbiadu un\'àudiu. + As imbiadu un\'àudiu. + %1$s at imbiadu un\'immàgine. + As imbiadu un\'immàgine. + Proa sa connessione de su serbidore + Agiorna %1$s sa base de datos tua + No at fatu a importare su contu seletzionadu + Su ligòngiu a s\'interfache de rete tua %1$s cando dd\'aberis in su navigadore. + Importa contu dae s\'aplicatzione %1$s + Importa contu + Importa contos dae s\'aplicatzione %1$s + Importa contos + Porta su %1$s tuo a sa mantenidura + Agabba %1$s s\'installatzione tua + Proa de connessione + Su serbidore no at suportadu s\'aplicatzione Talk installada + Indiritzu de su serbidore https://… + %1$s funtzionat isceti cun %2$s 13 e prus + Cunfigura una crae noa + Cunfiguratzione + Su contu chi tenias giai est istadu agiornadu, imbetzes de nd\'agiùnghere unu nou + Avantzadu + Aspetu + Mutidas + Imparat a sa tastiera a disativare s\'imparu personale (chene garantzias) + Tastiera in incògnita + Perunu sonu + S\'aplicatzione de mutidas no est installada in su serbidore a su chi as proadu a autenticare + Notìficas + Messàgios + Verìfica is cuntatos subra sa base de su nùmeru de telèfonu pro integrare su curtziadòrgiu de Talk in sìaplicatzione de is cuntatos de sistema + Podes cunfigurare su nùmeru de telèfonu tuo, in manera chi àteras utèntzias t\'ant a pòdere agatare + Inserta nùmeru de telèfonu + Nùmeru de telèfonu non bàlidu + Nùmeru de telèfonu cunfiguradu + Nùmeru de telèfonu + Integratzione de su nùmeru de telèfonu + Riservadesa + Retzidore Proxy + Crae proxy + Ghenna de su proxy + Genia de proxy + Nùmene utente proxy + Cumpartzi s\'istadu de letura miu e mustra cussu de àtere + Leghe istadu + Torra a autorizare su contu + Boga + Boga·nche su contu + Cunfirma s\'intentzione tua de nde bogare su contu atuale. + Bloca%1$s cun su blocu de ischermu Android o unu mètodu biomèricu suportadu + Tempus de inatividade de su blocu de ischermu + Blocu de ischermu + Impedit is caturas de ischermu in sa lista reghente e a intro de s\'aplicatzione + Seguresa ischermu + Sa versione de su serbidore est betza meda e no at a èssere suportada in sa versione imbeniente! + Sa versione de su serbidore est betza meda e non suportada dae custa versione de aplicatzione Android + Serbidore non suportadu + Iscuru + Imprea cunfiguratziones predefinidas de sistema + tema + Craru + Tema + Su proxy rechedet credentziales + Avisu + Isceti su contu atuale si podet torrare a autorizare + Cumpartzi sa positzione atuale + Cumpartzi ligòngiu + Cumpartzi positzione + Cumpartzi custa positzione + Sèbera contu + Ischeda in Deck + Positzione + Positzione cumpartzida + Assenta segundu + Ora de cumintzu + Passa a àteru contu + Sèbera documentos + Imbiare custos archìvios a %1$s? + Imbiare custu archìviu a %1$s? + Carrigamentu faddidu + Cumpartzidu dae %1$s + Càrriga dae su dispositivu + Carrigamentu + Utente + Registratzione Talk dae %1$s (%2$s) + Podera pro registrare, lassa pro imbiare. + Permissu pro registratziones àudio rechertu + « Iscurre pro annullare + Seminàriu in lìnia + Eja + Sa chida chi benit + Peruna integratzione de nùmeru de telèfonu pro farta de permissos + Predefinidu + 1 ora + In lìnia + Istadu in lìnia + Aberre is resonadas + Aberi in s\'aplicatzione de is archìvios + Riprodue/pausa messàgiu de boghe + Agiunghe sèberu + Optziones + Resurtados + Cunfiguratzione + Àrtzia sa manu + Totu + Non faghet a cumpartzire archìvios dae s\'archiviatzione chene permissos + Registratzione + Sarva + Scan QR Code + Sincroniza cun serbidores seguros ebbia + Federadu + Visìbile isceti a is persones invitadas e a is de custa istàntzia + Locale + Visìbile isceti a is persones agatadas cun integratzione de su nùmeru de telèfonu tràmite Talk in mobile + Privadu + Sincroniza cun serbidores seguros e sa rubrica globale e pùblica + Publicadu + Càmbiu de àmbitu + Càmbia su livellu de riservadesa de %1$s + Iscurre a bàsciu + segundos a immoe + Seletzionadu + Imbia messàgiu de posta eletrònica + Imbia a + Imbia a … + Cunfigura + Cunfigura un\'istadu + Cunfigura su messàgiu de istadu + Cumpartzi + Àudio + Archìviu + Media + Àteru + Boghe + Preferidu + Messàgiu de istadu + Tira una foto + Imbia + Càmbia fotocàmera + Ativa sa tortza + 30 minutos + Custa chida + Risponde + Oe + Cras + Borta + Cunfiguratzione de su dispositivu + Impossìbile rilevare sa lìngua + Dae + A + De lèghere + Càrriga un\'avatar nou dae dispositivu + Avatar de s\'utente + Indiritzu + Nùmene e sambenadu + Posta eletrònica + Nùmeru de telèfonu + Twitter + Situ ìnternet + Status + No at fatu a otènnere informatziones personales de s\'utente + Nissuna cunfiguratzione de datos personales + Agiunghe nùmene, fotografia e detàllios de cuntatu in su profilu tuo. + Mutida de vìdeo + Cale est s\'istadu tuo? + diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml new file mode 100644 index 0000000..e8b6f60 --- /dev/null +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -0,0 +1,666 @@ + + + Upraviť + Pridať + Pridať do poznámok + Pridať konverzáciu %1$s k obľúbeným + Hľadať v %s + Archivovať konverzáciu + Keď je konverzácia archivovaná, bude predvolene skrytá. Ak chcete zobraziť archivované konverzácie, vyberte filter „Archivované“. Priame zmienky budú stále prijímané. + Archivované + Archivované %1$s + Bluetooth + Zvukový výstup + Telefón + Reproduktor + Drátové sluchátka + Váš status bol nastavený automaticky + Avatar + Preč + Tlačítko späť + Ban + Udeliť účastníkovy ban + Zoznam banov + Zaneprázdnený + Kalendár + Pokročilé možnosti hovoru + Hovor už trvá viac ako jednu hodinu. + Volať bez upozornenia + Povolenie fotoaparátu bolo udelené. Vyberte fotoaparát znova. + Zrušiť prihlasovanie + Vyberte si avatara z cloudu + Vyčistiť správu o stave + Vyčistiť správu o stave po + Zatvoriť + Ikona Zavrieť + Pripojenie vytvorené + Pripojenie bolo stratené - Odoslané správy počkajú v rade + Zamknúť nahrávanie pre nepretržité nahrávanie hlasovej správy + Konverzácia je len na čítanie + Nepodarilo sa nastaviť konverzáciu na Iba na čítanie + Konverzácie + Vytvoriť konverzáciu + Vytvoriť problém + Vlastný + Nebezpečná oblasť + %1$s v %2$s + Zmazať avatara + Odstránená konverzácia %1$s + Nerušiť + Nemazať + Upraviť + Upraviť správu + Nedávne + Šifrované + Ukončiť hovor + Ukončiť hovor pre všetkých + Nastal problém s načítaním vašich správ + Pri rušení banu pre účastníka sa vyskytla chyba + Nepodarilo sa uložiť %1$s + 15 minút + priečinok + Načítavam … + %1$s (%2$d) + 4 hodiny + Nepodarilo sa načítať čakajúce pozvánky + (editované) + Interná poznámka + Neviditeľný + Jazyky sa nedali získať + Získanie zlyhalo + Neskôr dnes + Opustiť hovor + Opustili ste konverzáciu %1$s + Načítať viac výsledkov + Zamknúť konverzáciu + Symbol zámku + Dať ruku dole + Konverzácia %1$s bola označená ako prečítaná + Konverzácia %1$s bola označená ako neprečítaná + Spomenutý/á + Najnovšie prvé + Najstaršie prvé + A - Z + Z - A + Najväčšie prvé + Najmenšie prvé + Správu ste odstránili + Upravil%1$s + Klepnutím otvoríte anketu + Žiadne výsledky vyhľadávania + Začnite písať pre vyhľadanie … + Hľadať … + Správy + Vybraný účet je teraz importovaný a dostupný + O aplikácii + Aktívny užívateľ + Pridať účet + Účet je naplánovaný na zrušenie a preto nemôže byť zmenený + Otvoriť hlavné menu + Pridať prílohu + Pridať emotikony + Pridať do konverzácie + Pridať účastníkov + Pridať do obľúbených + OK, všetko hotovo! + Špendlík: %1$s + Odomknúť %1$s + Ak chcete povoliť reproduktory bluetooth, udeľte povolenie „Zariadenia v blízkosti“. + Dvihnúť ako video hovor + Dvihnúť iba ako audio hovor + Zmeniť audio výstup + Prepnúť kameru + Položiť + Prepnúť mikrofón + Otvoriť režim obraz-v-obraze + Prepnúť na vlastné video + PRICHÁDZAJÚCI + Názov konverzácie + Upozornenia na hovory + %1$s sa prihlásil(a) + Znova pripájam … + ZVONÍ + %1$s v rozhovore + %1$s s telefónom + %1$s s videom + Žiadna odpoveď počas 45 sekúnd, ťuknite a skúste znova + %s volanie + %s video hovor + %s hlasové volanie + Ak chcete povoliť videokomunikáciu, udeľte povolenie „Kamera“. + Zrušiť + Nepodarilo sa načítať funkcie, prerušujem + Popis + Dôverujete tomuto doteraz neznámemu SSL certifikátu, vydaného %1$s pre %2$s, platného od %3$s do %4$s ? + Skontrolujte certifikát + Vaša konfigurácia SSL zabránila pripojeniu + Zmeniť autentifikačný certifikát + Zmeniť heslo + Zrušiť úpravy + Zrušiť úpravy + Vymazať všetky správy + Všetky správy boli vymazané + Naozaj chcete vymazať všetky správy v tejto konverzácii? + Zmeniť klientský certifikát + Nastaviť klientský certifikát + a + Kopírovať + Skopírované do schránky + Vytvoriť + Vypnuté + Odmietnuť + Prepáčte, niečo sa nepodarilo! + Viac možností + Nastaviť + Preskočiť + Neznámy + Vybrať autentifikačný certifikát + Pripájam sa … + Hotovo + Popis konverzácie + Informácia o konverzácii + Video hovor + Hlasový hovor + Konverzácia sa nenašla + Nastavenia konverzácie + Pripojte sa ku konverzácii alebo začnite novú + Pozdravte svojich priateľov a kolegov! + Kopírovať + Vytvoriť novú konverzáciu + Vytvoriť anketu + Dnes + Včera + Zmazať + Vymazať všetko + Zmazať konverzáciu + Ak zmažete konverzáciu, bude takisto zmazaná pre všetkých ostatných zúčastnených + Zmazať správu + Správa úspešne odstránená, ale mohla uniknúť na iné serveri. + Užívateľ %1$s bol vymazaný + Odobrať moderovanie + Nahrať hlasovú správu + Odoslať správu + Aktuálny účet + Server + Je nainštalovaná aplikácia Upozornenia servera? + Používateľ + Stav používateľa je povolený? + Verzia systému Android + Aplikácia + Názov aplikácie + Registrovaní používatelia + Verzia aplikácie + Optimalizácia batérie je ignorovaná, všetko v poriadku + Optimalizácia batérie je povolená, čo môže spôsobiť problémy. Mali by ste vypnúť optimalizáciu batérie! + Nastavenia batérie. + Zariadenie + Otvoriť kontrolný zoznam na riešenie problémov + Otvoriť diagnostickú obrazovku + Otvoriť dontkillmyapp.com + Naposledy vyvolaný push token na platforme Firebase + Najnovšia generácia push tokenov na platforme Firebase + Nie je nastavená žiadna súprava push tokenov Firebase. Vytvorte hlásenie o chybe. + Firebase push token + Služby Google Play nie sú k dispozícii. Upozornenia nie sú podporované + služby Google Play + K dispozícii sú služby Google Play + Posledná push registrácia na push proxy + Zatiaľ nie je zaregistrovaný na serveri push proxy + Posledná push registrácia na serveri + Ešte nie je zaregistrovaný na serveri + Meta informácie + Generovanie systémovej správy + Je povolený kanál upozornení pre hovory? + Je povolený kanál upozornení pre správy? + Povolenia upozornení + Telefón + Verzia servera Talk + Verzia servera + Externý + Interné + Režim signalizácie + Neplatné heslo + Server je momentálne v režime údržby. + Aplikácia je neaktuálna + Aplikácia je príliš stará a tento server ju viac nepodporuje. Aktualizujte, prosím. + Aktualizovať + Prajete si znova autorizovať alebo zmazať tento účet? + Uložením tohto média do úložiska umožníte prístup k nemu všetkým ostatným aplikáciám vo vašom zariadení. + Pokračovať? + Nie + Uložiť do úložiska? + Áno + Zobrazované meno sa nedalo načítať, ruším + Nedá sa uložiť zobrazované meno, ruším + Upraviť + Úprava + Upraviť správu + Upravené adminom + 8 hodín + 4 týždne + Vypnúť + 1 deň + 1 hodina + 1 týždeň + Vypršanie platnosti správ v chate + Platnosť četových správ môže po určitom čase vypršať. Poznámka: Súbory zdieľané v čete sa vlastníkovi neodstránia, ale už sa nebudú zdieľať v konverzácii. + Nepodarilo sa načítať nastavenia signalizácie + Prijať + Odmietnuť + od %1$s o %2$s + Žiadne čakajúce pozvánky + Máte čakajúce pozvánky + Späť + Vyžaduje sa oprávnenie na prístup k súborom + Používateľ z verejného odkazu + Vy: %1$s + Preposlať + Preposlať na … + Galéria + Ešte nemáte server? \nKliknite sem a získajte jeden od poskytovateľa + Získajte zdrojový kód + Skupina + Hosť + Prístup pre hostí + Nie je možné zapnúť/vypnúť prístup pre hostí. + Povoliť hosťom zdieľať verejný odkaz na pripojenie k tejto konverzácii. + Povoliť hostí + Zadajte heslo + Heslo pre prístup hostí + Chyba pri nastavovaní/deaktivácii hesla. + Nastaviť heslo pre obmedzenie kto sa môže pripojiť cez verejný odkaz. + Ochrana heslom + Znovy odslať pozvánky + Pozvánky neboli odoslané kvôli chybe. + Pozvánky boli odoslané. + Zdieľať odkaz na konverzáciu + Vložte správu … + Optimalizácia batérie nie je ignorovaná. Toto by sa malo zmeniť, aby ste sa uistili, že upozornenia fungujú na pozadí! Kliknite na tlačidlo OK a vyberte možnosť Všetky aplikácie -> %1$s -> Neoptimalizovať + Ignorovať optimalizáciu batérie + Dôležitá konverzácia + Pozvánky + Pripojiť sa k prebiehajúcim konverzáciam + Skôr než budete môcť opustiť rozhovor, je potrebné niekomu odovzdať rolu moderátora. + %1$s | Posledná úprava: %2$s + Opustiť konverzáciu + Opúšťam hovor … + GNU Všeobecná Verejná Licencia, Verzia 3 + Licencia + Limit maximálneho počtu znakov %s dosiahnutý + Miestnosť + Tento rozhovor je naplánovaný na %1$s + Rozhovor začne čoskoro + Momentálne čakáte v miestnosti. + Vaša súčasná poloha + vyžaduje sa prístup k polohe + Neznáma poloha + Uzamknuté + Dotknite sa pre odomknutie + Nenastavené + Označiť ako prečítané + Označiť ako neprečítané + Zlyhalo + Chyba pri posielaní správy: + Odpojené + Zrušiť odpoveď + Správa bola prečítaná + Posielam + Správa bola odoslaná + Ak chcete povoliť hlasovú komunikáciu, udeľte povolenie „Mikrofón“. + Zmeškali ste hovor od %s + Moderátor + Nová konverzácia + Viditeľnosť + Neprečítané upozornenia + Neprečítané správy + %1$s nie je dostupné (nie je nainštalované alebo je obmedzené administrátorom) + Hosť + Nie + Žiadne prebiehajúce konverzácie + Momentálne neexistuje žiadna prebiehajúca konverzácia, ku ktorej by ste sa mohli pripojiť\nBuď žiadne neprebiehajú alebo sa už všetkých zúčastňujete. + Bez proxy + Nemáte oprávnenie povoliť zvuk! + Nemáte oprávnenie povoliť video! + Teraz nie + %1$s na %2$s notifikačný kanál + Hovory + Upozorniť na príchodzie hovory + Správy + Upozorniť na príchodzie správy + Nahrané + Upozorniť na priebeh nahrávania + Nastavenie notifikácií + Upozornenia nie sú korektne nastavené + Povolenie upozornení a nastavenia batérie sú správne nastavené na prijímanie upozornení. Ak aj tak máte problémy s prijímaním upozornení, skontrolujte, či sú povolené kanály upozornení pre hovory a správy. Ďalšiu pomoc možno nájsť na DontKillMyApp.com alebo v kontrolnom zozname na riešenie problémov. Ak to nepomôže, prejdite na obrazovku diagnostiky a pošlite správu o chybe. + Riešenie problémov s upozorneniami + Notifikuj vždy + Notifikuj pri spomenutí + Nikdy nenotifikuj + Práve ste odpojený. Prosím, skontrolujte vaše pripojenie. + OK + Otvoriť konverzáciu pre registrovaných užívateľov + Povoliť i pre užívateľov aplikácií pre hostí + Vlastník + Účastníci + Pridať účastníkov + Heslo + Nastaviť oprávnenia + Niektoré oprávnenia neboli udelené. + Prosím, udeľte oprávnenia + Otvoriť nastavenia + Udeľte oprávnenia v časti Nastavenia > oprávnenia + Účet sa nenašiel + Chatovanie cez %s + Stíšiť mikrofón + Povoliť mikrofón + Správy + Súkromie + Osobné informácie + Povýšiť na moderátora + Verejný rozhovor + Push notifikácie vypnuté + Stlač a hovor PTT + S vypnutým mikrofónom, kliknite &podržte pre použitie funkcie Stlač a hovor PTT + Pripomenúť neskôr + Odstrániť z obľúbených + Odobrať skupinu a členov + Odobrať účastníka + Odobrať heslo + Odstrániť tím a členov. + Premenovať konverzáciu + Premenovať + Odpoveď + Odpovedať súkromne + Uložiť + Úspešne uložené + 30 sekúnd + 5 minút + 1 minúta + 10 minút + Okamžite + 600 + 60 + 30 + 300 + Hľadať + Vymazať hľadanie + Zvoľte si účet + %1$s poslal(a)  GIF. + Odoslali ste GIF. + %1$s poslal(a) video. + Odoslali ste video. + %1$s poslal(a) zvuk. + Odoslali ste zvuk. + %1$s poslal(a) obrázok. + Odoslali ste obrázok. + %1$s poslal kartu deck + Test serverového pripojenia + Prosím aktualizujte vašu %1$sdatabázu + Nepodarilo sa importovať vybrané kontá + Odkaz k vašemu %1$s webovému rozhraniu keď ho otvoríte v prehliadači. + Importovať účet z aplikácie %1$s + Importovať účet + Importovať účty z aplikácie %1$s + Importovať účty + Prosím vypnite váš %1$s z režimu údržby + Prosím dokončite vašu %1$s inštaláciu + Testuje sa pripojenie + Na serveri nie je nainštalovaná podporovaná apka Talk + Adresa servera https://… + %1$spracuje iba s %2$s13 a vyššie + Vytvoriť nové heslo + Nastaviť Heslo + Nastavenia + Namiesto pridania nového účtu, bol aktualizovaný existujúci účet + Rozšírené + Vzhľad + Hovory + Prosím kontaktujte administrátora od + Otvorte diagnostickú obrazovku a skontrolujte nastavenia alebo vytvorte správu o chybe + Diagnóza + Klávesnica bude pracovať bez personalizovaného učenia (bez záruky) + Súkromná klávesnica + Žiadny zvuk + Aplikácia Talk nie je na serveri, voči ktorému ste sa chceli overiť nainštalovaná. + Upozornenia + Upozornenia sú odmietnuté + Upozornenia sú povolené + Správy + Priradiť kontakty podľa telefónneho čísla a integrovať odkaz na Rozhovor do aplikácie systémových kontaktov + Chyba 429 Príliš veľa požiadaviek + Môžete nastaviť vaše telefónne číslo aby vás ostatní používatelia našli + Zadajte telefónne číslo + Zlé telefónne číslo + Telefónne číslo bolo úspešne nastavené + Telefónne číslo + Integrácia telefónneho čísla + Súkromie + Proxy host + Heslo proxy + Proxy port + Typ proxy + Používateľ proxy + Zdieľať môj status o prečítaní a zobraziť status o prečítaní ostatných + Stav čítania + Znova autorizovať účet + Odstrániť + Odstrániť účet + Potvrďte prosím, že naozaj chcete odstrániť súčasné konto. + Zamknúť %1$s so zámkou obrazovky systému Android alebo podporovanou biometrickou metódou. + Zamknúť obrazovku po uplynutí času nečinnosti + Zámok obrazovky + Nepovolí screenshot v zozname posledných aplikácií a v aplikácii + Zabezpečenie obrazovky + Verzia tohoto serveru je veľmi stará a nebude podporovaná v ďalšej verzii! + Verzia tohoto serveru je veľmi stará a nebude podporovaná v ďalšej verzii Android aplikácie! + Nepodporovaný server + Aplikácia upozornení zo servera nie je nainštalovaná + Tmavý + Použiť systémové nastavenia + motív vzhľadu + Svetlý + Motív vzhľadu + Zdieľať môj stav písania a zobrazovať stav písania pri ostatných používateľoch + Stav písania je dostupný len pri použití vysokovýkonného backendu (HPB) + Stav písania + Proxy vyžaduje prilhlasovacie údaje + Varovanie + Iba aktuálny účet môže byť znova prihlásený + Zdieľať kontakt + Je potrebné oprávnenie čítať kontakty + Zdieľať súčasnú polohu + Zdieľať odkaz + Zdieľať polohu + Zdieľať túto polohu + Zvoliť účet + Zdieľané položky + Karta aplikácie deck + Obrázky, súbory, hlasové správy … + Žiadne zdieľané položky + Umiestnenie + Zdieľaná poloha + V príprade nesprávne nastavených upozornení, zobraziť štandartné upozornenie + Zobraziť štandartné upozornenie + Zoradiť podľa + Začať skupinový chat + Čas začiatku + Prepnúť účet + Tím + Vyberte súbory + Poslať tieto súbory do %1$s? + Poslať tento súbor do %1$s? + Prepáčte, nahrávanie zlyhalo + Nahrávanie %1$s zlyhalo + Zlyhanie + Zdieľať z %1$s + Nahrať zo zariadenia + Nahrávanie + %1$s do %2$s - %3$s\%% + Urobiť fotografiu + Natočiť video + Používateľ + Nahrávanie vidia od %1$s + Nahrávanie hovoru od %1$s (%2$s) + Podržte pre nahrávanie, uvoľnite pre poslanie. + Je potrebné oprávnenie pre nahrávanie zvuku. + « Potiahnite pre zrušenie + Webinár + Áno + Nasledujúci týždeň + Žiadne správy pri odpojení neboli uložené + Registrácia telefónneho čísla nie je povolená pre chýbajúce oprávnenia + Iba zmienky v tvare @-meno + Vypnúť + Predvolené + 1 hodina + Pripojený + Stav pripojenia + Otvoriť konverzácie + Otvoriť v aplikácii Súbory + Prehrať/pozastaviť hlasovú správu + Ovládanie rýchlosti prehrávania + Pridajte možnosť + Upraviť hlasovanie + Ukončiť anketu + Naozaj chcete ukončiť toto hlasovanie? Toto nie je možné vrátiť späť. + Nie je možné vybrať viac možností v tejto ankete, + Viac odpovedí + Odstrániť možnosť %1$d + Možnosť %1$d + Možnosti + Súkromná anketa + Otázka + Vaša otázka + Výsledky + Nastavenia + Hlas + Hlasovanie odoslané + Predtým nastavené + Zdvihnúť ruku + Všetko + Zdieľanie súborov z úložiska nie je možné bez oprávnenia + Hovor sa nahráva + Zrušiť začiatok nahrávania + Nahrávanie zlyhalo. Prosím, kontaktujte správcu. + Spustiť nahrávanie + Naozaj chcete zastaviť nahrávanie? + Zastaviť nahrávanie hovoru + Zastaviť nahrávanie + Zastavujem nahrávanie... + Súhlas so záznamom je potrebný pre všetky hovory. + Nahrávanie môže zahŕňať váš hlas, video z kamery a zdieľanie obrazovky. Pred pripojením sa k hovoru je potrebný váš súhlas. súhlasíte? + Vyžadovať súhlas so záznamom pred pripojením hovoru v tejto konverzácii + Súhlas so záznamom + Hovor môže byť zaznamenaný. + Záznam + Konverzácia %1$s bola odstránená z obľúbených + Konverzácia %1$s bola premenovaná + Znova odoslať + Obnoviť status + Počas hovoru nie je možné pripojiť sa k iným miestnostiam + Uložiť + Scan QR Code + Synchronizovať iba s dôveryhodnými servermi + Združený + Viditeľné iba pre ľudí tejto inštancie a návštevníkov + Lokálny + Viditeľné iba pre ľudí s integráciou telefónneho čísla prostredníctvom aplikácie Rozhovor pre mobil + Súkromný + Synchronizovať s dôveryhodnými servermi a globálnym a verejným adresárom + Publikované + Prepnúť prehľad + Zmeniť úroveň súkromia pre %1$s + Posuňte sa nadol + Ikona vyhľadávania + sekúnd dozadu + Vybrané + Odoslať email + Odoslať do + Nemáte oprávnenie zdieľať obsah tohto rozhovoru + Odoslať do … + Poslať bez upozornenia + Nastaviť + Nastaviť avatara z fotoaparátu + Nastaviť stav + Nastaviť správu o stave + Sprístupniť + Pridať sa ku konverzácii %1$s na %2$s + Zvuk + Súbor + Média + Iné + Anketa + Nahrávanie hovoru + Odkazová schránka + Zobraziť dovod ban-u + Zobraziť zabanovaných účastníkov + Obľúbené + Nemáte oprávnenie začat rozhovor + začal(a) hovor + Správa o stave + Stav vrátený + Prepnúť do vyhradenej miestnosti + Prepnúť do hlavnej miestnosti + Urobiť fotografiu + Chyba pri vytváraní fotografie + Fotografovanie nie je možné bez oprávnení + Znova vytvoriť fotografiu + Odoslať + Prepnúť kameru + Orezať fotografiu + Zmenšiť obrázok + Prepnúť baterku + 30 minút + Tento týždeň + Toto je skúšobná správa + Tento víkend + Odpovedať + Dnes + Zajtra + Preložiť + Preklad + Kopírovať preložený text + Zistiť jazyk + Nastavenia zariadenia + Nepodarilo sa zistiť jazyk + Preklad zlyhal + Od + Pre + a jeden ďalší píše... + píšu... + píše... + a %1$s ďalších píšu... + Zrušiť archiváciu konverzácie + Keď bude zrušená archivácia konverzácie, bude predvolene znova zobrazená. + Zrušená archivácia %1$s + Zrušiť zákaz + Neprečítané + Nahrať nového avatara zo zariadenia + %1$s nie je v práci a nemusí odpovedať + %1$s dnes nie je v práci + Náhrada: + Avatar užívateľa + Adresa + Meno a priezvisko + Email + Telefónne číslo + Twitter + Webstránka + Stav + Zlyhalo načítanie osobných údajov používateľa. + Osobné informácie neboli nastavené + Pridaj meno, obrázok a kontakt na profilovú stránku. + Aký je váš stav? + + Zobraziť %d podobnú správu + Zobraziť %d podobné správy + Zobraziť %d podobných správ + Zobraziť %d podobných správ + + + %d hlas + %d hlasy + %d hlasov + %d hlasov + + diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml new file mode 100644 index 0000000..6fc359d --- /dev/null +++ b/app/src/main/res/values-sl/strings.xml @@ -0,0 +1,525 @@ + + + Uredi + Dodaj + Poišči v %s + Pokaže kot brez povezave + Arhivirano + Bluetooth + Odvod zvoka + Telefon + Zvočnik + Ožičene slušalke + Stanje je določeno samodejno + Podoba + Ne spremljam + Zasedeno + Koledar + Napredne možnosti klica + Pogovor traja že eno uro. + Klic brez obvestila + Dovoljenje za uporabo kamere je dodeljeno. Ponovno izberite kamero. + Izbor pogodbe iz oblaka + Počisti sporočilo stanja + Počisti sporočilo stanja po + Zapri + Povezava je vzpostavljena + Pogovori + Ustvari pogovor + Po meri + Nevarno območje + Izbriši podobo + Ne moti + Ne počisti + Uredi + Uredi sporočilo + Nedavno + Šifrirano + Končaj klic + Končaj klic za vse + Prišlo je do napake nalaganja pogovorov. + Shranjevanje %1$s je spodletelo. + 15 minut + mapa + Poteka nalaganje … + %1$s (%2$d) + 4 urah + (spremenjeno) + Drugim nevidno + Zapusti klic + Naloži več zadetkov + Krajevni čas: %1$s + Zakleni pogovor + Simbol zaklepa + Spusti roko + najprej najnovejše + najprej najstarejše + po abecedi naraščajoče A–Ž + po abecedi padajoče Ž–A + najprej največje + najprej najmanjše + Sporočilo ste izbrisali + Pritisnite za prikaz ankete + Ni zadetkov iskanja + Vpišite niz za iskanje … + Poišči … + Sporočila + Utiša vsa obvestila + Izbran račun je uvožen in na voljo za uporabo + O programu + Dejavni uporabnik + Dodaj račun + Račun je pripravljen za brisanje in ga ni mogoče spreminjati + Odpri glavni meni + Dodaj prilogo + Vstavi izrazno ikono + Dodaj v pogovor + Dodaj udeležence + Dodaj med priljubljene + Opravilo je uspešno končano! + Koda PIN: %1$s + Odkleni %1$s + Odgovori z video klicem + Odgovori kot zvokovni klic + Spremeni odvod zvoka + Preklopi kamero + Končaj klic + Preklopi mikrofon + Odpri način slike v sliki + Preklopi na pogled sebe + DOHODNI + Naslov pogovora + Obvestila klica + Oseba %1$s je dvignila roko + Poteka ponovno vzpostavljanje povezave … + ZVONJENJE + %1$s v klicu + %1$s v video klicu + %1$s v video klicu + Že 45 sekund ni odziva. Ponovite vzpostavitev povezave. + Klic %s + Glasovni klic %s + Glasovni klic %s + Prekliči + Pridobivanje podatkov je spodletelo. Opravilo bo preklicano. + Ali zaupate do sedaj neznanemu potrdilu SSL, izdanem s strani %1$s za %2$s, z obdobjem veljavnosti od %3$s do %4$s? + Preveri potrdilo + Nastavitve SSL onemogočajo vzpostavitev povezave + Spremeni potrdilo za overitev + Spremeni geslo + Prekliči urejanje + Prekliči urejanje + Izbriši vsa sporočila + Vsa sporočilo so izbrisana + Ali res želite izbrisati vsa sporočila tega pogovora? + Spremeni potrdilo odjemalca + Nastavi potrdilo odjemalca + in + Kopiraj + Kopirano v odložišče + Ustvari + Onemogočeno + Opusti + Prišlo je do napake … + Več možnosti + Nastavi + Preskoči + Neznano + Izbor potrdila za overitev + Poteka vzpostavljanje povezave … + Začni pogovor + Opis pogovora + Podrobnosti pogovora + Video klic + Glasovni klic + Nastavitve pogovora + Pridružite se pogovoru, ali pa začnite novega. + Pozdravite prijatelje in znance. + Kopiraj + Ustvari nov pogovor + Ustvari vprašalnik + Danes + Včeraj + Izbriši + Izbriši vse + Izbriši pogovor + Če izbrišete pogovor, bo ta izbrisan za vse udeležence. + Izbriši sporočilo + Sporočilo je uspešno izbrisano, a je lahko že poslano na druge storitve. + Izbriši + Ponižaj iz moderatorja + Posnemi glasovno sporočilo + Pošlji sporočilo + Trenutni račun + Strežnik + Uporabnik + Različica Android + Program + Ime programa + Vpisani uporabniki + Nastavitve baterije + Naprava + Telefon + Zunanja + Notranji + Neveljavno geslo + Posodobi + Ali želite ponovno overiti ali odstraniti račun? + Ne + Da + Prikaznega imena ni mogoče pridobiti; opravilo je prekinjeno. + Prikaznega imena ni mogoče shraniti; opravilo je prekinjeno. + Uredi + Uredi + Uredi sporočilo + 8 ur + 4 tedni + Onemogočeno + 1 dneva + 1 uri + 1 tedna + Preteči prikaz sporočil klepeta + Sporočila klepeta lahko po določenem času potečejo. Opomba: datoteke, ki so v skupni rabi v klepetu, se za lastnika ne izbrišejo, vendar se bodo več zbrane v pogovoru. + Pridobivanje signalnih nastavitev je spodletelo + Sprejmi + Zavrni + Nazaj + Zahtevano je dovoljenje za dostop do datoteke + Uporabnik, ki je sledil javni povezavi + → %1$s + Posreduj + Posreduj za … + Galerija + Še nimate izbranega oziroma nameščenega strežnika? Kliknite in si pridobite dostop. + Pridobi izvorno kodo + Skupina + Gost + Dostop gostov + Ni mogoče preklopiti možnosti dostopa za goste. + Dovoli gostujočim uporabo javne povezave do pogovora. + Dovoli udeležbo gostov + Vpis gesla + Dostopno geslo za goste + Prišlo je do napake med nastavljanjem ali onemogočanjem gesla. + Nastavi geslo za omejitev uporabe javne povezave. + Zaščita z geslom + Ponovno pošlji vabila + Vabila niso bila poslana zaradi napake. + Vabila so bila ponovno poslana. + Omogoči souporabo povezave pogovora + Vpis sporočila … + Pomemben pogovor + Povabila + Pridruži se odprtemu pogovoru + Ohrani + Pred odhodom iz pogovora je treba nekoga določiti za moderatorja. + %1$s | Nazadnje spremenjeno: %2$s + Zapusti pogovor + Poteka prekinjanje klica … + Splošno Javno dovoljenje GNU GPL, različice 3 + Dovoljenje + Dosežena je omejite %s znakov. + Spletna čakalnica + Začetek srečanja je načrtovan ob %1$s + Srečanje se bo kmalu začelo + Trenutno še čakate v spletni čakalnici. + Vaše trenutno mesto + zahtevano je dovoljenje objave trenutnega mesta + Položaj ni znan + Zaklenjeno + Pritisnite za odklep + Ni nastavljeno + Označi kot prebrano + Označi kot neprebrano + Opravilo je spodletelo! + Pošiljanje sporočila je spodletelo: + Brez povezave + Prekliči odgovor + Sporočilo je prebrano + Sporočilo je poslano + Zgrešili ste klic stika %s + Moderator + Nov pogovor + Vidnost + Neprebrane omembe + Neprebrana sporočila + %1$s ni na voljo (ni nameščen ali pa je dostop skrbniško omejen) + Gost + Ne + Brez posredniškega strežnika + Ni ustreznih dovoljenj za omogočanje zvoka + Ni ustreznih dovoljenj za omogočanje slike + Ne zdaj + %1$s na kanalu za obveščanje %2$s + Klici + Obvesti o prejetih klicih + Sporočila + Obvesti o prejetih sporočilih + Poslano + Obvesti o napredku pošiljanja + Nastavitve obveščanja + Vedno obvesti + Obvesti ob omembi + Nikoli ne obvesti + Ni omrežne povezave; preverite povezave! + V redu + Odpri pogovor vpisanim uporabnikom + Odpri tudi za gostujoče uporabnike + Lastnik + Udeleženci + Dodaj udeležence + Geslo + Odpri nastavitve + Računa ni mogoče najti + Klepet prek %s + Utišaj mikrofon + Omogoči mikrofon + Sporočila + Zasebnost + Osebni podatki + Povišaj v vlogo moderatorja + Javni pogovor + Potisno obveščanje je onemogočeno + Možnost »push-to-talk« + Pri onemogočenem mikrofonu lahko za začetek govora kliknete in zadržite gumb + Opomni me kasneje + Odstrani iz priljubljenih + Odstrani skupino in člane + Odstrani udeležence + Odstrani skupino in člane + Preimenuj pogovor + Preimenuj + Odgovori + Odgovori zasebno + Shrani + Uspešno shranjeno + 30 sekund + 5 minut + 1 minuta + 10 minut + 600 + 60 + 30 + 300 + Poišči + Počisti iskanje + Izbor računa + %1$s pošlje sličico GIF. + Pošljete sličico GIF + %1$s pošlje video datoteko. + Pošljete video sporočilo + %1$s pošlje zvočno datoteko. + Pošljete zvočno sporočilo + %1$s pošlje sliko. + Pošljete sliko + Preizkusi povezavo s strežnikom + Posodobite podatkovno zbirko %1$s + Uvoz izbranega računa je spodletel + Povezava do spletnega vmesnika %1$s, ki se odpre v brskalniku. + Uvozi račun iz %1$s + Uvozi račun + Uvozi račune iz %1$s + Uvozi račune + Končajte vzdrževalni način na %1$s. + Dokončajte namestitev %1$s + Preizkušanje povezave + Na strežniku ni nameščenega ustreznega programa Talk + Naslov strežnika: https:// + Program %1$s je podprt le na različicah %2$s 13 in novejših. + Nastavi novo geslo + Nastavitve + Že obstoječ račun je posodobljen, zato nov račun ni bil dodan + Napredno + Videz + Klici + Nastavi določilo za onemogočanje osebnega učenja (brez vsakršnih zagotovil) + Skrito tipkanje + Brez zvoka + Program Talk na strežniku, s katerim ga skušate overiti, ni nameščen. + Obvestila + Sporočila + Uskladi stike po telefonski številki za povezavo imenika stikov s programom Talk. + Nastavite telefonsko številko, da vas bodo drugi uporabniki lahko našli. + Vpis telefonske številke + Neveljavna telefonska številka + Telefonska številka je uspešno nastavljena + Telefonska številka + Združevalnik telefonskega imenika + Zasebnost + Gostitelj posredniškega strežnika + Geslo posredniškega strežnika + Vrata posredniškega strežnika + Vrsta posredniškega strežnika + Uporabniško ime posredniškega strežnika + Objavi moje stanje branja in pokaži tudi stanja drugih uporabnikov + Stanje branja + Ponovno overi dostop do računa + Odstrani + Odstrani račun + Potrdite zahtevo za odstranitev trenutnega računa. + Zakleni %1$s z zaklepom zaslona oziroma drugo podprto biometrično metodo + Časovni zamik zaklepa ob nedejavnosti + Zaklep zaslona + Prepreči ustvarjanje zaslonskih slik v seznamu nedavnih pogovorov in znotraj programa + Varnost zaslona + Različica strežnika je zastarela in ne bo več podprta z naslenjo različico programa. + Različica strežnika je zastarela in ni več podprta v tej različici programa za okolje Android. + Nepodprta različica strežnika + Temna + Uporabi sistemsko privzeto možnost + tema + Svetla + Tema + Stanje tipkanja + Posredniški strežnik zahteva ustrezna poverila + Opozorilo + Ponovno overiti je mogoče le trenutno dejaven račun + Posreduj podatke stika + Zahtevano je dovoljenje za branje seznama stikov + Objavi trenutno mesto + Povezava za souporabo + Objavi trenutno mesto + Objavi trenutno mesto + Izbor računa + Predmeti v souporabi + Naloga Deck + Slike, datoteke, glasovna sporočila ... + Ni predmetov v souporabi + Trenutno mesto + Objavljeno trenutno mesto + Razvrsti po + Čas začetka + Preklopi račun + Skupina + Izbor datotek + Ali želite poslati te datoteke osebi %1$s? + Ali želite poslati to datoteko osebi %1$s? + Pošiljanje je spodletelo. + Pošiljanje %1$s je spodletelo + Spodletelo + Souporaba %1$s + Pošlji z naprave + Poteka pošiljanje + %1$s za %2$s – %3$s\%% + Zajemi sliko + Zajemi video + Uporabnik + Posnetek %1$s (%2$s) + Zadržite za snemanje in sprostite za pošiljanje. + Zahtevano je dovoljenje za zvočno snemanje. + « Potegnite za preklic + Spletinar + Da + Naslednji teden + Povezava s telefonskim imenikom ni na voljo zaradi neustreznih dovoljenj. + Privzeto + 1 uri + Trenutno na spletu + Povezano stanje + Odprti pogovori + Odpri v programu Datoteke + Predvajaj/Ustavi zvočni posnetek + Dodaj možnost + Uredi glasovanje + Končaj anketo + Ali res želite končati anketo? Dejanja ni mogoče povrniti. + Ni mogoče izbrati več možnosti te ankete. + Več odgovorov + Možnosti + Zasebna anketa + Vprašanje + Vaše vprašanje + Zadetki + Nastavitve + Glas + Oddan glas + Predhodno nastavljeno + Dvigni roko + Vse + Skupna raba datotek iz pomnilnika ni mogoča brez dovoljenj + Klic se snema. + Prekliči začenjanje snemanja + Snemanje je spodletelo. Stopite v stik s skrbnikom sistema. + Začni s snemanjem + Ali res želite ustaviti snemanje? + Ustavi snemanje klica + Ustavi snemanje + Strinjanje s snemanjem + Klic se morda snema. + Snemanje + Ponastavi stanje + Shrani + Scan QR Code + Usklajuj le z zaupanja vrednimi strežniki + Zvezno + Vidno le krajevnim uporabnikom in povezanim gostom + Krajevno + Vidno le uporabnikom povezanimi s programom Talk prek telefonske številke. + Zasebno + Usklajuj z varnimi strežniki in splošnimi in javnimi imeniki + Objavljeno + Preklop obsega + Spremeni pravila zasebnosti za %1$s + Podrsajte do dna + pred nekaj sekundami + Izbrano + Pošlji elektronsko sporočilo + Pošlji + Ni ustreznih dovoljenj za objavljanje vsebine v ta klepet + Pošlji … + Pošlji brez obvestila + Nastavi + Nastavi sličico s kamero + Nastavi stanje + Nastavi sporočilo stanja + Souporaba + Zvočni posnetek + Datoteka + Predstavna vsebina + Drugo + Anketa + Posnetek klica + Glasovna pošta + Priljubljeno + Ni ustreznih dovoljenj za začetek klica + Sporočilo stanja + Preklopi na ločene skupine + Preklopi na skupno mesto + Zajemi sliko + Napaka zajemanja slike + Zajemanje slik brez ustreznih dovoljenj ni mogoče + Ponovno zajemi sliko + Pošlji + Preklopi kamero + Obreži sliko + Zmanjšaj velikost slike + Preklopi bliskavico + 30 minut + Ta teden + Danes + Jutri + Prevodi + Prevod + Kopiraj prevedeno besedilo + Zaznava jezika + Nastavitve naprave + Ni mogoče zaznati jezika. + Prevajanje je spodletelo + Od + Za + Neprebrano + Posodobi podobo z naprave + Zamenjava + Podoba uporabnika + Naslov + Polno ime + Elektronsko sporočilo + Telefonska številka + Račun Twitter + Spletna stran + Stanje + Pridobivanje uporabniških podrobnosti je spodletelo. + Osebni podatki še niso vpisani + Dodajte ime, slike in podrobnosti na profilno stran. + Kako želite nastaviti stanje? + diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000..b617a4d --- /dev/null +++ b/app/src/main/res/values-sr/strings.xml @@ -0,0 +1,731 @@ + + + Измени + Додај + Додај у Белешке + Разговор %1$s је додат у омиљене + Тражи у %s + Прикажи као ван мреже + Архивирај разговор + Када се разговор архивира, подразумевано ће бити сакривен. Да бисте видели архивиране разговоре, изаберите филтер „Архивирано”. И даље ће се примати директна помињања. + Архивирано + %1$s је архивиран + Гласовни позив + Bluetooth + Аудио излаз + Телефон + Звучник + Жичане слушалице са микрофоном + Ваш статус је аутоматски постављен + Аватар + Одсутан + Дугме назад + Забрана + Забрани учесника + Листа забрањених + Заузет + Календар + Напредне опције позива + Позив траје један сат. + Позив без обавештења + Дозвољен је приступ камери. Молимо вас да поново изаберете камеру + Откажи пријављивање + Изаберите аватар из облака + Обриши статусну поруку + Обриши статусну поруку након + Затвори + Икона затвори + Веза успостављена + Нема везе са сервером + Веза је прекинута - Послате поруке су стављене у ред + Закључајте снимање да би се гласовна порука непрестано снимала. + Разговор је архивиран + Овај разговор је само-за-читање + Разговор није могао да се постави као само-за-читање + Разговори + Креирај разговор + Креирај проблем + Прилагођено + Зона опасности + %1$s у %2$s + Обриши аватар + Обриши снимак гласа + Разговор %1$s је обрисан + Не узнемиравај + Не бриши + Измени + Поруке старије од 24 часова не могу да се уређују + Уреди поруку + Недавно + Шифровано + Заврши позив + Заврши позив за све + Дошло је до проблема приликом учитавања ваших четова + Дошло је до грешке приликом укидања забране за учесника + Није успело чување %1$s + 15 минута + фасцикли + Учитавање… + %1$s (%2$d) + Низови које пратите + 4 сата + Није успело добављање позивница на чекању + (уређено) + Интерна белешка + Невидљива + Није успело преузимање језика + Није успело преузимање + Касније данас + Напусти разговор + Напустили сте разговор %1$s + Учитај још резултата + Локално време: %1$s + Одбијена је дозвола за локацију + Молимо вас да је укључите у подешавањима апликације + Искључени су сервиси локације + Да бисте користили ову функционалност, молимо вас да укључите сервисе локације (GPS) + Закључај разговор + Симбол катанца + Спуштена рука + Разговор %1$s је означен као прочитан + Разговор %1$s је означен да није прочитан + Поменут + Прво новије + Прво старије + А - Ш + Ш - А + Прво највеће + Прво најмање + Порука је копирана + Да ли сте сигурни да желите да обришете ову поруку? + Обрисали сте поруку + Уредио је %1$s + Тапните да отворите гласање + Нема резултата претраге + Крените да куцате и претрага почиње ... + Претрага ... + Поруке + Искључи сва обавештења + Одабрани налог је сада увезен и доступан + О програму + Активни корисник + Додај налог + Налогу је заказано брисање и то не може да се промени + Отвори главни мени + Додај прилог + Додај емођије + Додај у разговор + Додај учеснике + Додај у омиљене + У реду, све готово! + Прикачено: %1$s + Откључај %1$s + Ако желите да укључите bluetooth звучнике, молимо вас да дате дозволу „Оближњи уређаји” + Одговори као видео позив + Одговори као гласовни позив + Промени аудио излаз + Укљ./искљ. камеру + Прекини везу + Укљ./искљ. микрофон + Отвори режим слика-у-слици + Пређи на сопствени видео + ДОЛАЗНО + Назив разговора + Обавештења позива + %1$s је подигао руку + Поновно повезивање ... + ЗВОНИ + %1$s је у позиву + %1$s је на телефону + %1$s са видеом + Нема одговора у 45 секунди, притисните да пробате поново + %s позив + %s видео позив + %s гласовни позив + Ако желите да укључите видео комуникацију, молимо вас да дате дозволу „Камера” + Откажи + Грешка приликом дохватања могућности, прекидам + Наслов + Да ли верујете, до сада непознатом, SSL сертификату, који је издао %1$s на %2$s, валидним од %3$s до %4$s? + Провери сертификат + Ваша ССЛ постава је онемогућила повезивање + Измени сертификат за пријаву + Промени лозинку + Откажи уређивање + Откажи уређивање + Обриши све поруке + Обрисане су све поруке + Да ли заиста желите да обришете све поруке у овом разговору? + Измени клијентски сертификат + Подесите клијентски сертификат + и + Копирај + Копирано у клипборд + Креирање + Искључено + Уклони + Извињавамо се, нешто је пошло наопако! + Више опција + Постави + Preskoči + Непознато + Одабери сертификат за пријаву + Повезивање .. + Готово + Опис разговора + Информације о разговору + Видео позив + Аудио позив + Разговор није нађен + Подешавања разговора + Придружи се разговору или започни нови + Поздравите Ваше пријатеље и колеге! + Копија + Креирај нови разговор + Направи гласање + Ви: + Данас + Јуче + Обриши + Обриши све + Обриши разговор + Ако обришете разговор, он ће бити обрисан и за све друге учеснике. + Обриши поруку + Порука је успешно обрисана, али је можда процурела на друге сервисе + Обриши одмах + Корисник %1$s је уклоњен + Скини улогу модератора + Сними гласовну поруку + Пошаљи поруку + Тренутни налог + Сервер + Да ли је инсталирана апликација серверских обавештења? + Корисник + Да ли је укључен статус корисника? + Верзија Андроида + Апликација + Назив апликације + Регистровани корисници + Верзија апликације + Игнорише се оптимизација батерије, све је у реду + Укључена је оптимизација батерије и то може да буде узрок проблема. Требало би да искључите оптимизацију батерије. + Подешавања батерије + Уређај + Отвори контролну листу за отклањање проблема + Отвори дијагностички екран + Отвори dontkillmyapp.com + Добављање најновијег firebase push жетона + Генерисање најновијег firebase push жетона + Није постављен ниједан firebase push жетон. Молимо вас да креирате извештај о багу. + Firebase push жетон + Нису доступни Google Play сервиси. Не подржавају се обавештења + Google Play сервиси + Google Play services су доступни + Најновија push регистрација на push прокси + Још увек није регистровано на push прокси + Најновија push регистрација на сервер + Још увек није регистровано на сервер + Мета информације + Генерисање системског извештаја + Да ли је укључен канал обавештења о позивима? + Да ли је укључен канал обавештења о порукама? + Дозволе за обавештења + Телефон + Верзија Talk сервера + Верзија сервера + Спољно + Интерни + Режим сигнализирања + Неисправна лозинка + Сервер је тренутно у режиму одржавања. + Апликација је застарела + Ова апликација је превише стара и овај сервер је више не подржава. Молимо вас да је ажурирате. + Ажурирај + Да ли желите поново да ауторизујете или обришете овај налог? + Ако овај медијум сачувате на складиште, остале апликације на уређаје ће моћи да му приступе. + Желите ли да наставите? + Не + Да сачувам на складиште?` + Да + Име за приказ се не може добавити. Прекидам + Не могу да сачувам име за приказ. Прекидам + Измени + Измени + Уреди поруку + Уредио је админ + Мени разговора у догађају + Закажи + 8 сати + 4 недеље + Искључена + 1 дан + 1 сат + 1 недеље + Истек чек порука + Чет поруке могу да истекну након одређеног времена. Напомена: фајлови који су подељени у чету се неће обрисати за власника, али више неће бити дељени у разговору. + Грешка приликом дохватања подешавања сигнализирања + Прихвати + Одбаци + од %1$s са %2$s + Нема позивница на чекању + Имате позивнице на чекању + Назад + Потребна је дозвола за приступ фајлу + Филтрирај разговоре + Корисник који је пратио јавну везу + Ти: %1$s + Проследи + Прослеђивање на … + Галерија + Имате ли сервер?nКликните овде да направите један код провајдера + Изворни кôд + Група + Гост + Приступ госта + Приступ госта не може да се укључи/искључи. + Дозвољава да гости деле јавни линк за приступ овом разговору. + Дозволи госте + Унесите лозинку + Лозинка за приступ госта + Грешка приликом постављања/уклањања лозинке. + Постављање лозинке којом се ограничава употреба јавног линка. + Заштита лозинком + Поново пошаљи позивнице + Позивнице нису послате услед грешке. + Позивнице су поново послате. + Дели линк разговора + Unesite poruku … + Оптимизација батерије се не игнорише. Ово би требало да се промени тако да би обавештења могла да функционишу у позадини! Молимо вас да кликнете OK и изаберете „Све апликације” -> %1$s -> Не оптимизуј + Игнориши оптимизацију батерије + Битан разговор + Кориснички статус „Не узнемиравај” се игнорише за важне разговоре + Неисправно време + Позивнице + Придружи се отвореном разговору + Задржи + Морате да промовишете новог модератора пре него што напустите разговор + %1$s | Последње измењено: %2$s + Напусти разговор + Напуштање позива ... + ГНУ општа јавна лиценца, верзија 3 + Лиценца + Достигнут је лимит од %s карактера + Лоби + Овај састанак је заказан за %1$s + Састанак ће ускоро да почне + Тренутно чекате у лобију. + Ваша тренутна локација + неопходна је дозвола локације + Непозната позиција + Закључано + Тапни за откључавање + Није постављено + Означи као прочитано + Означи као непрочитано + Разговор је означен као важан + Разговор више није означен као осетљив + Разговор је означен као осетљив + Разговор више није означен као важан + Састанак је завршен + У белешке је додата порука + Није успело + Није успело слање поруке: + Ван мреже + Откажи одговор + Порука је прочитана + Шаље се + Порука послата + Микрофон је укључен и снима се звук + Ако желите да укључите аудио комуникацију, молимо вас да дате дозволу „Микрофон” + Имате пропуштен позив од %s + Модератор + Нови разговор + Видљивост + Непрочитана помињања + Непрочитане поруке + %1$s није доступно (није инсталирано или је ограничио админ) + Гост + Не + Нема отворених разговора + Нема ниједног отвореног разговора којем можете да се прикључите.\nИли нема отворених разговора, или сте се већ прикључили у све. + Без посредника + Није вам дозвољено да активирате аудио! + Није вам дозвољено да активирате видео! + Не сад + %1$s на каналу за обавештење о %2$s + Позиви + Обавести ме о долазним позивима + Поруке + Обавести ме о долазним порукама + Отпремања + Обавештавај ме о напретку отпремања + Поставке обавештења + Обавештења нису исправно подешена + Дозволе за обавештења и подешавања батерије су исправно подешени за примање обавештења. Ако ипак имате проблеме са пријемом обавештења, молимо вас да проверите да ли су укључени канали обавештења за позиве и поруке. Више помоћи можете да пронађете на DontKillMyApp.com или у контролној листи за отклањање проблема. Ако ово не помогне, молимо вас да одете на дијагностички екран и пошаљете извештај о багу. + Отклањање проблема у вези са обавештењима + Увек обавести + Обавести на помињањe + Никад не обавештавај + Тренутно ван мреже, проверите да ли имате интернет + У реду + Састанак је у току + Отвори разговор свим регистрованим корисницима + Такође отвори и за кориснике апликације гост + Власник + Учесници + Додај учеснике + Лозинка + Постави дозволе + Неке дозволе нису одобрене. + Молимо вас да дате дозволе + Отвори поставке + Молимо вас да одобрите доволе у Подешавања > Дозволе + Налог није нађен + Чет преко %s + Утули микрофон + Укључи микрофон + Поруке + Приватност + Лични подаци + Унапреди у модератора + Јавни разговор + Брза обавештења су искључена + Жао нам је, нешто је пошло наопако. Грешка је %1$s + Жао нам је, нешто је пошло наопако, тест брзо обавештење не може да се преузме + Брзо обавештење је успешно послато. Сада би требало да примите обавештење на овај уређај под називом ’Тестирање брзих обавештења’ + Притисни за разговор + Са онемогућеним микрофоном, кликните & држите да користите притисак за разговор + Подсети ме касније + Уклони из омиљених + Уклони групу и чланове + Уклони учесника + Уклони лозинку + Уклони тим и чланове + Преименуј разговор + Преименуј + Одговори + Одговори приватно + Соба је успешно задржана + Сачувај + Успешно сачувано + 30 секунди + 5 минута + 1 минут + 10 минута + Одмах + 600 + 60 + 30 + 300 + Тражи + Обриши претрагу + Изаберите налог + Ажурирај поруку + Пошаљи снимак гласа + Осетљиви разговор + Преглед поруке ће се сакрити у листи разговора и у обавештењима + %1$s је послао GIF. + Послали сте GIF. + %1$s је послао видео. + Послали сте видео. + %1$s је послао звук. + Послали сте звук. + %1$s је послао слику. + Послали сте слику. + %1$s је послао картицу шпила + Испробај везу са сервером + Ажурирајте своју %1$s базу + Грешка при учитавању одабраног налога + Линк на ваш %1$s веб интерфејс када га отворите у прегледачу. + Увези налог из апликације %1$s + Увези налог + Увези налоге из апликације %1$s + Увези налоге + Прекините %1$s режим одржавања + Довршите своју %1$s инсталацију + Проверавам везу + Сервер нема инсталирану подржану Talk апликацију + Адреса сервера https://… + %1$s ради само са%2$s верзије 13 и већом + Постави нову лозинку + Постави лозинку + Поставке + Ваш постојећи налог је ажуриран, уместо што је додат нови налог + Напредно + Изглед + Позиви + Молимо вас да контактирате администратора + Отвара дијагностички екран да проверите поставке или да креирате извештај о багу + Дијагностика + Наложи тастатури да искључи персонализовано учење (без гаранција) + Инкогнито тастатура + Нема звука + Апликација Ћаскања није инсталирана на серверу на који се пријављујете + Обавештења + Обавештења су одбијена + Обавештења су дозвољена + Поруке + Подудара контакте према броју телефона тако да се Talk пречица интегрише у системску апликацију контаката + Грешка 429 Превише захтева + Можете да поставите свој број телефона тако да остали корисници могу да вас пронађу + Унесите број телефона + Неисправан број телефона + Број телефона је успешно постављен + Број телефона + Интеграција броја телефона + Приватност + Домаћин + Прокси лозинка + Порт посредника + Тип посредника + Прокси корисничко име + Дели мој статус читања и приказуј статус читања осталих + Статус читања + Поново ауторизуј налог + Уклони + Уклони налог + Потврдите да желите да уклоните тренутни налог. + Закључајте %1$s са Андроидовим екраном закључавања или подржаним биометријским методом + Период неактивности до закључавања екрана + Закључавање екрана + Забрани снимке екрана у скорашњој листи и унутар апликације + Безбедност екрана + Верзија сервера је врло стара и неће бити подржана у наредном издању! + Верзија сервера је врло стара и није подржана у овој верзији Андроид апликације + Неподржани сервер + Апликација серверских обавештења није инсталирана + Постави чувара батерије + Тамна + Користи системске подразумеване + тема + Светла + Тема + Дели мој статус куцања и приказуј статус куцања осталих + Статус куцања је доступан само када се користи позадински механизам високих перформанси (HPB) + Статус куцања + Прокси захтева креденцијале + Упозорење + Само тренутни налог се може поново ауторизовати + Дели контакт + Неопходна је дозвола за читање контаката + Дели тренутну локацију + Подели везу + Дели локацију + Подели ову локацију + Одаберите налог + Дељене ставке + Картица Шпила + Слике, фајлови, говорне поруке ... + Нема дељених ставки + Локација + Дељена локација + Када обавештења нису исправно подешена, прикажи уобичајено упозорење + Прикажи уобичајено упозорење о обавештењу + Разврстај + Покрени групни чет + Време почетка + Пребаци налог + Тим + Тестирање брзих обавештења + Резултати теста + Данас у %1$s + Сутра у %1$s + Изаберите фајлове + Да ли да пошаљем ове фајлове %1$s? + Да ли да пошаљем овај фајл %1$s? + Нажалост, отпремање није успело + %1$s није могло да се отпреми + Неуспех + Дељење од %1$s + Отпреми са уређаја + Отпремање + %1$s ка %2$s - %3$s\%% + Сликај + Направи видео снимак + Корисник + Видео снимак од %1$s + Говорни снимак од %1$s (%2$s) + Задржите за снимање, отпустите да пошаљете. + Потребна је дозвола за аудио снимање + « Превуците да откажете + Вебинар + Да + Наредне недеље + Нема архивираних разговора + Није сачувана ниједна порука за ван мреже + Нема интеграције броја телефона јер недостају дозволе + Све поруке + Само @-помињања + Искључено + Подразумевано + Прати подешавања разговора + 1 сат + На мрежи + Мрежни статус + Отвори разговор + Отвори у апликацији Фајлови + Отвори Белешке + Иди на нит + Пусти/паузирај гласовну поруку + Контрола брзине репродукције + Додај ставку + Уреди гласање + Заврши гласање + Да ли заиста желите да завршите ово гласање? То не може да се поништи. + У овом гласању не можете да изаберете више одговора. + Вишеструки одговори + Обриши опцију %1$d + Опција %1$d + Избори + Приватно гласање + Питање + Ваше питање + Резултати + Поставке + Глас + Глас је предат + Претходно постављено + QR кôд није могао да се прочита + Подигни руку + Све + Дељење фајлова из складишта није могуће без дозвола + Скорашње нити + Разговор се снима + Откажи почетак снимања + Снимање није успело. Молимо вас да се обратите свом администратору. + Почни снимање + Да ли заиста желите да зауставите снимање? + Прекидање снимање позива + Заустави снимање + Снимање се зауставља ... + Сагласност за снимање се захтева за све позиве + Може се снимати ваш глас, видео са камере и садржај екрана који делите. Пре него што приступите позиву, морате дати своју сагласност. Да ли пристајете? + Захтевај сагласност за снимање пре приступања позиву у овом разговору + Сагласност за снимање + Овај позив би могао да се снима. + Снимање + Разговор %1$s је уклоњен из омиљених + Промењен је наслов разговора %1$s + Пошаљи поново + Ресетуј статус + Док сте у позиву не можете да приступите осталим собама + Сачувај + Scan QR Code + Само синхронизује са серверима од поверења + Здружено + Видљиво је само људима на овој инстанци и гостима + Локално + Видљиво је само људима који су пронађени интеграцијом броја телефона кроз Разговор на мобилном + Приватно + Синхронизуј на сервере од поверења и глобалне и јавне адресаре + Објављено + Пребацивање опсега важења + Промена нивоа приватности за %1$s + Скролуј на дно + Икона претраге + пре неколико секунди + Одабрано + Пошаљи е-пошту + Пошаљи у + Није вам дозвољено да делите садржај у овај чет + Пошаљи у ... + Пошаљи без обавештења + Постави + Постави аватар са камере + Постави статус + Постави статусну поруку + Подели + Придружите се разговору %1$s на %2$s + Звук + Фајл + Мултимедија + Остало + Гласање + Снимање позива + Гласовна пошта + Прикажи разлог забране + Прикажи забрањене кориснике + Омиљени + Није вам дозвољене да започнете позив + Креирај нит + је започео позив + Порука стања + Враћено је старо стање статуса + Пређи у сепаре + Пређи у главну собу + Сликај + Грешка приликом прављења слике + Прављење слике није могуће без дозвола + Поново направи слику + Пошаљи + Пребаци камеру + Опсеци слику + Умањи величину слике + Пребаци стање лампе + 30 минута + Ове недеље + Ово је тест порука + Овог викенда + Откажи креирање нити + Обавештења низова + Одговори + Наслов нити + Није пронађен ниједан низ + Данас + Сутра + Превођење + Превод + Копирај преведени текст + Откриј језик + Подешавања уређаја + Не може да се детектује језик + Превођење није успело + Од + За + and и још 1 куцају… + куцају... + куца... + и %1$s других куцају… + Деархивирај разговор + Када се разговор деархивира, поново ће се подразумевано приказивати. + %1$s је враћен из архиве + Уклони забрану + Непрочитано + Отпреми нови аватар са уређаја + %1$s је ван канцеларије у можда неће одговорити + %1$s је данас ван канцеларије + Замена: + Кориснички аватар + Адреса + Пуно име + Е-пошта + Број телефона + Твитер + Веб страна + Статус + Није успело добављање личних података о кориснику. + Нису постављени лични подаци + Додајте име, слику и детаље за контакт на страницу профила. + Видео позив + Који је ваш статус? + + Погледајте %d сличну поруку + Погледајте %d сличне поруке + Погледајте %d сличних порука + + + Овај разговор ће се аутоматски обрисати за све након %1$d дана неактивности + Овај разговор ће се аутоматски обрисати за све након %1$d дана неактивности + Овај разговор ће се аутоматски обрисати за све након %1$d дана неактивности + + + %d одговор + %d одговора + %d одговора + + + %d глас + %d гласа + %d гласова + + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml new file mode 100644 index 0000000..316ba5b --- /dev/null +++ b/app/src/main/res/values-sv/strings.xml @@ -0,0 +1,722 @@ + + + Redigera + Lägg till + Lägg till i anteckningar + Lade till konversationen %1$s till favoriter + Sök i %s + Visa som frånkopplad + Arkivera konversation + När en konversation väl har arkiverats döljs den som standard. Välj filtret \"Arkiverad\" för att se arkiverade konversationer. Direkta omnämnanden kommer fortfarande att tas emot. + Arkiverad + Arkiverad %1$s + Ljudsamtal + Bluetooth + Ljudutgång + Telefon + Högtalare + Sladdansluten hörlur + Din status ställdes in automatiskt + Avatar + Borta + Tillbakaknapp + Blockera + Blockera deltagare + Blockeringslista + Upptagen + Kalender + Avancerade samtalsalternativ + Samtalet har pågått i en timme. + Samtal utan avisering + Kameratillstånd beviljad. Välj kamera igen. + Avbryt inloggning + Välj avatar från moln + Rensa statusmeddelande + Rensa statusmeddelande efter + Stäng + Anslutning etablerad + Ingen anslutning till servern + Anslutning förlorad - Skickade meddelanden är i kö + Lås inspelning för kontinuerlig inspelning av röstmeddelandet + Konversationen är arkiverad + Konversationen är skrivskyddad + Kunde inte sätta konversationen skrivskyddad + Konversationer + Skapa konversation + Skapa ärende + Anpassad + Riskzon + %1$s i %2$s + Ta bort avatar + Ta bort röstinspelning + Raderade konversationen %1$s + Stör ej + Rensa inte + Ändra + Meddelanden som är äldre än 24 timmar kan inte redigeras + Redigera meddelande + Senaste + Krypterad + Avsluta samtal + Avsluta samtal för alla + Det gick inte att ladda dina chattar + Fel uppstod när deltagare skulle avblockeras + Misslyckades att spara %1$s + 15 minuter + mapp + Läser in … + %1$s (%2$d) + Följda trådar + 4 timmar + Kunde inte hämta väntande inbjudningar + (redigerad) + Intern anteckning + Osynlig + Kunde inte hämta språk + Hämtning misslyckades + Senare idag + Lämna samtalet + Du lämnade konversationen %1$s + Visa fler resultat + Lokal tid: %1$s + Behörighet för plats nekad + Aktivera det i appens inställningar + Platstjänster inaktiverat + Aktivera platstjänster (GPS) för att använda denna funktion + Lås konversation + Låssymbol + Ta ner handen + Markerade konversationen %1$s som läst + Markerade konversationen %1$s som oläst + Omnämnd + Nyast först + Äldst först + A - Ö + Ö - A + Störst först + Minst först + Meddelandet kopierat + Är du säker på att du vill ta bort detta meddelande? + Meddelandet togs bort av dig + Redigerad av %1$s + Tryck för att öppna omröstningen + Inga sökresultat + Börja skriva för att söka ... + Sök ... + Meddelanden + Stäng av alla aviseringar + Valt konto är nu importerat och tillgängligt + Om + Aktiv användare + Lägg till konto + Kontot är schemalagt för radering och kan inte ändras + Öppna huvudmenyn + Lägg till bilaga + Lägg till emojis + Lägg till i konversation + Lägg till deltagare + Lägg till som favorit + Ok, allt klart! + Pin: %1$s + Lås upp %1$s + För att aktivera bluetooth-högtalare måste du bevilja tillstånd för \"Enheter i närheten\". + Svara som videosamtal + Svara endast som röstsamtal + Ändra ljudutgång + Växla kamera + Lägg på + Växla mikrofon + Öppna bild i bild-läge + Växla till egen video + INKOMMANDE + Samtalsnamn + Samtalsaviseringar + %1$s räckte upp handen + Återansluter … + RINGER + %1$s i samtal + %1$s med telefon + %1$s med video + Inget svar på 45 sekunder, tryck för att försöka igen + %s samtal + %s videosamtal + %s röstsamtal + För att aktivera videokommunikation måste du bevilja \"Kamera\"-tillstånd. + Avbryt + Misslyckades att hämta funktioner, avbryter + Rubrik + Litar du på det tills nu okända SSL-certifikatet, skapad av %1$s för %2$s, giltig från%3$s till %4$s? + Kontrollera certifikatet + Dina SSL-inställningar förhindrade anslutning + Ändra autentiseringscertifikat + Ändra lösenord + Avbryt redigering + Avbryt redigering + Radera alla meddelanden + Alla meddelanden raderades + Vill du verkligen ta bort alla meddelanden i den här konversationen? + Ändra klientcertifikat + Konfigurera klientcertifikat + och + Kopiera + Kopierat till urklipp + Skapa + Inaktiverad + Avfärda + Ledsen, någonting gick fel! + Fler alternativ + Sätt + Hoppa över + Okänd + Välj autentiseringscertifikat + Ansluter… + Färdig + Konversationsbeskrivning + Konversationsinfo + Videosamtal + Röstsamtal + Konversationen hittades inte + Konversationsinställningar + Anslut till en konversation eller starta en ny + Säg hej till dina vänner och kollegor! + Kopia + Skapa en ny konversation + Skapa omröstning + Du: + Idag + Igår + Ta bort + Ta bort alla + Ta bort konversation + Om du raderar konversationen, kommer den även att raderas för alla andra deltagare. + Radera meddelande + Meddelandet har raderats, men kan ha läckt till andra tjänster + Ta bort nu + Användare %1$s togs bort + Degradera från moderator + Spela in röstmeddelande + Skicka meddelande + Nuvarande konto + Server + Aviseringsappen installerad på servern? + Användare + Användarstatus aktiverad? + Androidversion + App + Appnamn + Registrerade användare + Appversion + Batterioptimering ignoreras, allt är bra + Batterioptimering är aktiverad vilket kan orsaka problem. Du bör inaktivera batterioptimering! + Batteriinställningar + Enhet + Öppna checklista för felsökning + Öppna diagnosskärmen + Öppna dontkillmyapp.com + Senaste hämtning av firebase push-token + Senaste generering av firebase push-token + Ingen firebase push-token satt. Vänligen skapa en felrapport. + Firebase push-token + Google Play-tjänster är inte tillgängliga. Aviseringar stöds inte + Google Play-tjänster + Google Play-tjänster är tillgängliga + Senaste push-registrering på push-proxy + Ännu inte registrerad på push-proxy + Senaste push-registrering på server + Ännu inte registrerad på servern + Metainformation + Generering av systemrapport + Aviseringskanal för samtal aktiverad? + Aviseringskanal för meddelanden aktiverad? + Aviseringsbehörigheter + Telefon + Server Talk-version + Serverversion + Extern + Intern + Signaleringsläge + Felaktigt lösenord + Servern är för närvarande i underhållsläge. + Appen är föråldrad + Appen är för gammal och stöds inte längre av den här servern. Uppdatera. + Uppdatera + Vill du återauktorisera eller ta bort det här kontot? + Spara detta media till lagring kommer tillåta alla andra appar på din enhet åtkomst till det. + Fortsätt? + Nej + Spara till lagring? + Ja + Visningsnamn kunde inte hämtas, avbryter + Kunde inte lagra visningsnamnet, avbryter + Redigera + Redigera + Redigera meddelande + Redigerad av admin + Menyn för händelsekonversation + Schemalägg + 8 timmar + 4 veckor + Av + 1 dag + 1 timme + 1 vecka + Chattmeddelanden löper ut + Chattmeddelanden kan löpa ut efter en viss tid. Notera: Filer som delas i chatten kommer inte att raderas för ägaren, men kommer inte längre att delas i konversationen. + Misslyckades med att hämta signaleringsinställningar + Acceptera + Avvisa + från %1$s %2$s + Inga väntande inbjudningar + Du har väntande inbjudningar + Tillbaka + Tillstånd för filåtkomst krävs + Filtrera konversationer + Användare följer en offentlig länk + Du: %1$s + Vidarebefordra + Vidarebefordrar till … + Galleri + Har du inte en server än?\nKlicka här för att få en från en leverantör + Hämta källkod + Grupp + Gäst + Gäståtkomst + Kan inte aktivera/inaktivera gäståtkomst. + Tillåt gäster att dela en offentlig länk för att gå med i den här konversationen. + Tillåt gäster + Ange ett lösenord + Lösenord för gäståtkomst + Fel under inställning/inaktivering av lösenordet. + Ange ett lösenord för att begränsa vem som kan använda den offentliga länken. + Lösenordsskydd + Skicka inbjudningar igen + Inbjudningar skickades inte på grund av ett fel. + Inbjudningar skickades ut igen. + Dela konversationslänk + Skriv meddelande ... + Batterioptimering ignoreras inte. Detta bör ändras för att säkerställa att aviseringar fungerar i bakgrunden! Klicka på OK och välj \"Alla appar\" -> %1$s -> Optimera inte + Ignorera batterioptimering + Viktigt samtal + Användarstatus \"Stör ej\" ignoreras för viktiga konversationer + Ogiltig tid + Inbjudningar + Gå med i öppna konversationer + Behåll + Du måste tilldela en ny moderator innan du kan lämna konversationen + %1$s | Senast ändrad: %2$s + Lämna konversationen + Lägger på … + GNU General Public License, Version 3 + Licens + %s teckengränsen har uppnåtts + Lobby + Det här mötet är planerat till %1$s + Mötet börjar snart + Du väntar för närvarande i lobbyn. + Din nuvarande plats + platstillstånd krävs + Position okänd + Låst + Tryck för att låsa upp + Inte inställd + Markera som läst + Markera som oläst + Konversationen markerades som viktig + Konversationen avmarkerades som känslig + Konversationen markerades som känslig + Konversationen avmarkerades som viktig + Möte avslutat + Meddelande tillagt i anteckningar + Misslyckades + Kunde inte skicka meddelande: + Frånkopplad + Avbryt svar + Meddelande läst + Skickar + Meddelande skickat + Mikrofonen är aktiverad och ljud spelas in + För att aktivera röstkommunikation ange \"Mikrofon\"-tillstånd. + Du missade ett samtal från %s + Moderator + Ny konversation + Synlighet + Olästa omnämnanden + Olästa meddelanden + %1$sinte tillgänglig (inte installerad eller begränsad av admin) + Gäst + Nej + Inga öppna konversationer + Inga öppna konversationer som du kan gå med i.\nAntingen finns det inga öppna konversationer eller så har du redan gått med i alla. + Ingen proxy + Du får inte aktivera ljud! + Du får inte aktivera video! + Inte nu + %1$s på %2$s aviseringskanal + Samtal + Avisera vid inkommande samtal + Meddelanden + Avisera vid inkommande meddelanden + Uppladdningar + Avisera om uppladdningsförlopp + Meddelandeinställningar + Aviseringar är inte korrekt inställda + Aviseringsbehörighet och batteriinställningar är korrekt inställda för att ta emot aviseringar. Om du ändå har problem med att ta emot aviseringar, kontrollera om aviseringskanalerna för samtal och meddelanden är aktiverade. Ytterligare hjälp kan hittas på DontKillMyApp.com eller på checklistan för felsökning. Om detta inte hjälper, gå till diagnosskärmen och skicka en felrapport. + Felsökning aviseringar + Meddela alltid + Meddela när omnämnd + Meddela aldrig + För närvarande frånkopplad, kontrollera din anslutning + OK + Pågående möte + Öppna konversation för registrerade användare + Även öppen för gästappanvändare + Ägare + Deltagare + Lägg till deltagare + Lösenord + Ange behörigheter + Vissa behörigheter nekades. + Tillåt behörigheter + Öppna inställningar + Bevilja behörigheter under Inställningar > Behörigheter + Kontot hittades inte + Chatta via %s + Stäng av mikrofon + Aktivera mikrofon + Meddelanden + Integritet + Personlig information + Befordra till moderator + Publik konversation + Push-aviseringar inaktiverade + Tyvärr, något gick fel. Felet är %1$s + Tyvärr, något gick fel. Kunde inte hämta testmeddelande för push-notis + Push-notis skickades framgångsrikt. Du bör nu få en notis på den här enheten med titeln \'Testar push-notiser\' + Push-to-talk + Med mikrofon inaktiverad klickar du på &vänta för att använda Push-to-talk + Påminn mig senare + Ta bort från favoriter + Ta bort grupp och medlemmar + Ta bort deltagare + Ta bort lösenordet + Ta bort team och medlemmar + Byt namn på konversation + Döp om + Svara + Svara privat + Rummet har behållits framgångsrikt + Spara + Sparad + 30 sekunder + 5 minuter + 1 minut + 10 minuter + Omedelbar + 600 + 60 + 30 + 300 + Sök + Rensa sökning + Välj ett konto + Uppdatera meddelande + Skicka röstinspelning + Känslig konversation + Förhandsvisning av meddelanden kommer att inaktiveras i konversationslistan och i aviseringar + %1$s skickade en GIF. + Du skickade en GIF. + %1$s skickade en video. + Du skickade en video. + %1$s skickade en ljudfil. + Du skickade en ljudfil. + %1$s skickade en bild. + Du skickade en bild. + %1$s skickade ett Deck-kort + Testa serveranslutning + Vänligen uppgradera din %1$s-databas + Misslyckades att importera valt konto + Länken till din %1$s webbsida när du öppnar den i webbläsaren. + Importera konton från %1$s-appen + Importera konto + Importera konton från %1$s-appen + Importera konton + Vänligen inaktivera underhållningsläge för din %1$s + Vänligen slutför din %1$s-installation + Testar anslutning + Servern har inte korrekt Talk-app installerad + Serveradress https://… + %1$s fungerar bara med %2$s 13 och uppåt + Ange nytt lösenord + Ange lösenord + Inställningar + Ditt redan befintliga konto uppdaterades, istället för att lägga till ett nytt + Avancerat + Utseende + Samtal + Kontakta administratören av + Öppna diagnosskärmen för att kontrollera inställningarna eller skapa en felrapport + Diagnos + Instruerar tangentbordet att inaktivera personlig inlärning (utan garantier) + Inkognito-tangentbord + Inget ljud + Talk-appen är inte installerad på den server som du försöker autentisera mot + Aviseringar + Aviseringar avvisas + Anmälningar beviljas + Meddelanden + Matcha kontakter baserat på telefonnummer för att integrera Talk-genväg i systemkontaktappen + Fel 429 För många förfrågningar + Du kan ställa in ditt telefonnummer så att andra användare kan hitta dig + Ange telefonnummer + Ogiltigt telefonnummer + Telefonnumret har ställts in + Telefonnummer + Integrering av telefonnummer + Integritet + Proxy-värd + Proxylösenord + Proxy-port + Proxy-typ + Proxyanvändare + Dela min lässtatus och visa andras lässtatus + Lässtatus + Godkänn konto på nytt + Ta bort + Ta bort konto + Bekräfta att du vill radera aktuellt konto. + Lås %1$s med Android skärmslås eller biometrisk metod + Tidsgräns för skärmlås + Skärmlås + Förhindrar skärmdumpar i den senaste listan och inuti appen + Skärmsäkerhet + Serverversionen är mycket gammal och kommer inte att stödjas i nästa version! + Serverversionen är för gammal eller ej stödd av den här versionen av Andriod-appen + Serverversion stöds inte + Aviseringsappen är inte installerad på servern + Angivet av batterisparläge + Mörk + Använd systemets standard + tema + Ljus + Tema + Dela min skrivstatus och visa andras skrivstatus + Skrivstatus är endast tillgänglig när du använder en högpresterande backend (HPB) + Skrivstatus + Proxy kräver behörigheter + Varning + Bara nuvarande konto kan återautentiseras + Dela kontakt + Tillstånd att läsa kontakter behövs + Dela aktuell plats + Delningslänk + Dela plats + Dela den här platsen + Välj konto + Delade objekt + Deck-kort + Bilder, filer, röstmeddelanden ... + Inga delade objekt + Plats + Delad plats + När aviseringar inte är korrekt inställda, visa en vanlig varning + Visa vanlig varning för avisering + Sortera efter + Starta gruppchatt + Starttid + Växla konto + Team + Testa pushnotiser + Testresultat + Idag kl. %1$s + Imorgon kl. %1$s + Välj filer + Skicka dessa filer till %1$s? + Skicka denna fil till %1$s? + Tyvärr, uppladdningen misslyckades + Kunde inte ladda upp %1$s + Fel + Dela från %1$s + Ladda upp från enheten + Laddar upp + %1$s till %2$s - %3$s\%% + Ta en bild + Spela in video + Användare + Videoinspelning från %1$s + Talk-inspelning från %1$s (%2$s) + Håll ned för att spela in, släpp för att skicka. + Tillstånd för ljudinspelning krävs + « Dra för att avbryta + Webinar + Ja + Nästa vecka + Inga arkiverade konversationer + Inga offlinemeddelanden sparade + Ingen telefonnummerintegrering på grund av saknade behörigheter + Av + Förvald + Följ konversationsinställningar + 1 timme + Online + Online-status + Öppna konversationer + Öppna i appen Filer + Öppna anteckningar + Gå till tråd + Spela/pausa röstmeddelande + Kontroll av uppspelningshastighet + Lägg till alternativ + Redigera röst + Avsluta omröstning + Vill du verkligen avsluta den här omröstningen? Detta kan inte ångras. + Du kan inte rösta med fler alternativ för den här omröstningen. + Flera svar + Ta bort alternativ %1$d + Alternativ %1$d + Alternativ + Privat omröstning + Fråga + Din fråga + Resultat + Inställningar + Rösta + Röst inlämnad + Tidigare inställd + QR-koden kunde inte läsas + Räck upp handen + Alla + Att dela filer från lagring är inte möjligt utan behörighet + Senaste trådar + Samtalet spelas in + Avbryt start av inspelning + Samtalsinspelning misslyckades. Kontakta din administratör. + Starta inspelning + Vill du verkligen stoppa inspelningen? + Stoppa samtalsinspelning + Stoppa inspelning + Stoppar inspelningen ... + Samtycke för inspelning krävs för alla samtal + Inspelningen kan innehålla din röst, video från kameran och skärmdelning. Ditt samtycke krävs innan du går med i samtalet. Samtycker du? + Kräv inspelningssamtycke för att gå med i samtalet i den här konversationen + Samtycke för inspelning + Samtalet kan spelas in. + Inspelning + Tog bort konversationen %1$s från favoriter + Konversationen %1$s har bytt namn + Skicka igen + Återställ status + Det är inte möjligt att gå med i andra rum under ett samtal + Spara + Skanna QR-kod + Synkronisera endast med betrodda servrar + Federerad + Endast synlig för personer på denna instansen och gäster + Lokal + Endast synlig för personer matchade via telefonnummer integration via Talk i mobilen + Privat + Synkronisera med betrodda servrar och den globala och offentliga adressboken + Publicerad + Växla omfång + Ändra hemlighetsnivå för %1$s + Bläddra till botten + sekunder sedan + Vald + Skicka e-post + Skicka till + Du får inte dela innehåll till denna chatt + Skicka till … + Skicka utan avisering + Sätt + Ställ in avatar från kameran + Sätt status + Sätt statusmeddelande + Dela + Gå med i konversationen %1$s %2$s + Ljud + Fil + Media + Annat + Omröstning + Samtalsinspelning + Röst + Visa orsak till blockering + Visa blockerade deltagare + Favorit + Du får inte starta ett samtal + Skapa en tråd + startade ett samtal + Statusmeddelande + Status återställd + Byt till grupprum + Byt till huvudrum + Ta en bild + Det gick inte att ta bilden + Det är inte möjligt att ta ett foto utan behörighet + Ta om foto + Skicka + Växla kamera + Beskär foto + Minska bildstorlek + Starta lampa + 30 minuter + Denna vecka + Detta är ett testmeddelande + Denna helgen + Avbryt skapande av tråd + Trådaviseringar + Svara + Trådtitel + Idag + I morgon + Översätt + Översättning + Kopiera översatt text + Upptäck språk + Enhetsinställningar + Kunde inte identifiera språk + Översättning misslyckades + Från + Till + och 1 annan skriver ... + skriver ... + skriver ... + och %1$s andra skriver … + Avarkivera konversation + När en konversation har avarkiverats visas den som standard igen. + Avarkiverad %1$s + Tillåt + Oläst + Ladda upp ny avatar från enheten + %1$s är frånvarande och kanske inte svarar + %1$s är frånvarande idag + Ersättning: + Användar-avatar + Adress + Fullständigt namn + E-post + Telefonnummer + Twitter + Hemsida + Status + Kunde inte hämta personlig användarinformation. + Ingen personlig info inställd + Lägg till namn, bild och kontaktuppgifter på din profilsida. + Videosamtal + Vad är din status? + + Se %d liknande meddelande + Se %d liknande meddelanden + + + Den här konversationen kommer automatiskt att tas bort för alla om %1$d dag utan aktivitet + Den här konversationen kommer automatiskt att tas bort för alla om %1$d dagar utan aktivitet + + + %d svar + %d svar + + + %d röst + %d röster + + diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml new file mode 100644 index 0000000..cd866e3 --- /dev/null +++ b/app/src/main/res/values-sw/strings.xml @@ -0,0 +1,729 @@ + + + Hariri + Ongeza + Ongeza katika kumbukumbu + Mazungumzo yaliyoongezwa %1$skatika vipendwa + Tafuta katika %s + Tokea nje ya mtandao + Hifadhi mazungumzo + Mara tu mazungumzo yanapowekwa kwenye kumbukumbu, yatafichwa kwa chaguo-msingi. Chagua kichujio \"Kilichohifadhiwa\" ili kutazama mazungumzo yaliyohifadhiwa. Matangazo ya moja kwa moja bado yatapokelewa. + Imewekwa katika kumbukumbu + Imewekwa katika kumbukumbu %1$s + Muito wa sauti + Bluetooth + Kitolea sauti + Simu + Spika + Headset za waya + Hadhi yako ilipangiliwa moja kwa moja + Avata + Mbali + Batani ya nyuma + Piga marufuku + Mshiriki aliyepigwa marufuku + Orodha iliyopigwa marufuku + Bize + Kalenda + Chaguzi za simu za hali ya juu + Simu imekuwa ikifanya kazi kwa saa moja. + Piga simu bila arifa + Ruhusa ya kamera imetolewa. Tafadhali chagua kamera tena. + Ghairi uingiaji + Chagua avatar kutoka katika cloud + Futa jumbe za wadhifa + Futa jumbe za wadhifa baada ya + Funga + Funga aikoni + Muunganiko umeanzishwa + Hakuna mtandao kwenye seva + Muunganisho umepotea - Jumbe zilizotumwa zimewekwa kwenye foleni + Funga rekodi ili uendelee kurekodi ujumbe wa sauti + Mazungumzo yamehifadhiwa + Mazungumzo yanasomwa tu + Imeshindwa kuweka mazungumzo ya Kusomwa tu + Mazungumzo + Tengeneza mazungumzo + Tengeneza jambo + Mteja + Eneo la hatari + %1$s katika %2$s + Futa avata + Futa urekodiji wa sauti + Mazungumzo yaliyofutwa %1$s + Acha kusumbua + Usifute + Hariri + Jumbe zenye zaidi ya masaa 24 haziwezi kuhaririwa + Hariri ujumbe + Hivi karibuni + Imesimbwa kwa njia fiche + Kata simu + Maliza simu kwa kila mtu + Kulikuwa na tatizo kupakia chati zako + Hitilafu ilitokea wakati wa kubatilisha mshiriki + Imeshindwa kuhifadhi %1$s + Dakika 15 + folda + Inapakia + %1$s (%2$d) + Mazungumzo yaliyofuatwa + Masaa 4 + Imeshindwa kuleta mialiko ambayo haijashughulikiwa + (imehaririwa) + Nukuu ya ndani + Haionekani + Lugha hazikuweza kurejeshwa + Urejeshaji umeshindikana + Baadaye leo + Acha simu + Umeacha mazungumzo %1$s + Pakia matokeo zaidi + Muda wa kawaida: %1$s + Ruhusa ya eneo imekataliwa + Tafadhali iwashe katika mipangilio ya programu + Huduma za eneo zimezimwa + Tafadhali wezesha huduma za eneo (GPS) ili kutumia kipengele hiki + Funga mazungumzo + Funga ishara + Mkono wa chini + Mazungumzo yaliyowekwa alama kama yamesomwa%1$s + Mazungumzo yaliyowekwa alama kama yasiyosomwa %1$s + Imetajwa + Mpya kwanza + Ya zamani kwanza + A mpaka Z + Z mpaka A + Kubwa zaidi kwanza + Ndogo zaidi kwanza + Ujumbe umenakiliwa + Je, una uhakika unataka kufuta ujumbe huu? + Ujumbe umefutwa na wewe + Imehaririwa na %1$s + Bonyeza kufungua poll + Hakuna matokeo ya utafutaji + Anza kuchapa ili kutafuta + Tafuta... + Jumbe + Zima arifu zote + Akaunti iliyochaguliwa sasa imeingizwa na inapatikana + Kuhusu + Mtumiaji anayetumia + Ongeza akaunti + Akaunti imeratibiwa kufutwa, na haiwezi kubadilishwa + Fungua mwongozo mkuu + Ongeza kiambatisho + Ongeza emoji + Ongeza kwenye mazungumzo + Ongeza washiriki + Ongeza kwenye pendwa + Sawa, zote zimefanyika! + Pin: %1$s + Fungua %1$s + To enable bluetooth speakers please grant \"Nearby devices\" permission. + Jibu kama simu ya video + Jibu kama simu ya sauti tu + Badili maingilio ya sauti + Geuza kamera + Kata simu + Geuza microphone + Fungua picha katika modi ya picha + Hamia kwenye video binafsi + Zinazoingia + Jina la mazungumzo + Arifu za simu + %1$s ameinua mkono + Inaunganisha upya + INAITA + %1$s katika simu + %1$s na simu + %1$sna video + Hakuna jibu katika sekunde 45, bonyeza kujaribu tena + %s piga simu + %s simu ya video + %s simu ya sauti + Kuwezesha mawasiliano ya video tafadhali toa ruhusa ya kamera + Cancel + Imeshindwa kuleta uwezo, kukatisha + Manukuu + Je, unaamini cheti cha SSL kisichojulikana hadi sasa, kilichotolewa na %1$s kwa %2$s, halali kutoka %3$s hadi %4$s? + Angalia cheti + Usanidi wako wa SSL umezuia muunganisho + Badilisha cheti cha uthibitishaji + Badili nenosiri + Ghairi uhariri + Ghairi uhariri + Futa jumbe zote + Jumbe zote zilifutwa + Je, kweli unataka kufuta ujumbe wote katika mazungumzo haya? + Badili cheti cha mteja + Pangilia cheti cha mteja + na + Nakili + Nakili katika ubao wa kunakili + Tengeneza + Zima + Ondoa + Pole, kitu fulani kimeenda vibaya + Mibadala zaidi + Pangilia + Ruka + Haijulikani + Chagua cheti cha uthibitishaji + Inaunganisha... + Imefanyika + Maelezo ya mazungumzo + Taarifa za mazungumzo + Simu ya video + Simu ya sauti + Mazungumzo hayapo + Mipangilio ya mazungumzo + Jiunge na mazungumzo au anzisha mapya + Sema hi kwa marafiki zako na wenzako! + Nakili + Tengeneza mazungumzo mapya + Tengeneza poll + Wewe: + Leo + Jana + Futa + Futa zote + Futa mazungumzo + Ukifuta mazungumzo, yatafutwa pia kwa washiriki wengine wote. + Futa ujumbe + Ujumbe umefutwa kikamilifu, lakini huenda umevuja kwa huduma zingine + Futa sasa + Mtumiaji %1$s aliondolewa + Mshushe kutoka msimamizi + Rekodi ujumbe wa sauti + Tuma ujumbe + Akaunti ya sasa + Seva + Programu ya arifa ya seva imesakinishwa? + Mtumiaji + Je, hali ya mtumiaji imewezeshwa? + Toleo la Android + Programu + Jina la program + Watumiaji waliojisajili + Toleo la program + Uboreshaji wa betri umepuuzwa, yote sawa + Uboreshaji wa betri umewezeshwa ambayo inaweza kusababisha matatizo. Unapaswa kuzima uboreshaji wa betri! + Mipangilio ya betri + Kifaa + Fungua orodha hakiki ya utatuzi + Fungua skrini ya utambuzi + Fungua dontkillmyapp.com + Tokeni ya hivi punde ya kushinikiza ya firebase + Uzalishaji wa ishara za hivi karibuni za kushinikiza za firebase + Hakuna tokeni ya kushinikiza ya firebase iliyowekwa. Tafadhali tengeneza ripoti ya hitilafu. + Ishara ya kusukuma ya Firebase + Huduma za Google Play hazipatikani. Arifa hazitumiki + Huduma za Google play + Huduma za Google Play zipo + Usajili wa hivi punde wa kushinikiza kwenye proksi ya kushinikiza + Bado haijasajiliwa kwenye proksi ya kushinikiza + Usajili wa hivi karibuni wa kushinikiza kwenye seva + Bado haijasajiliwa katika seva + Taarifa za Meta + Uzalishaji wa ripoti ya mfumo + Njia ya arifu za simu imewezeshwa + Njia ya arifu za jumbe imewezeshwa + Ruhusa ya arifu + Simu + Toleo la seva ya Talk + Toleo la seva + Nje + Ndani + Namna ya kuashiria + Nenosiri batili + Seva iko katika hali ya matengenezo + Programu imepitwa na wakati + Programu ni ya zamani sana na haitumiki tena na seva hii. Tafadhali sasisha. + Sasisha + Je, ungependa kuidhinisha upya au kufuta akaunti hii? + Kuhifadhi maudhui haya kwenye hifadhi kutaruhusu programu zingine zozote kwenye kifaa chako kukifikia. + Endelea? + Hapana + Ungependa kuhifadhi kwenye hifadhi? + Ndiyo + Jina la onyesho halikuweza kuletwa, inaahirisha +  + Haikuweza kuhifadhi jina la onyesho, inakatisha + Hariri + Hariri + Hariri ujumbe + Imehaririwa na msimamizi + Mwongozo wa mazungumzo ya tukio + Ratiba + Masaa 8 + Wiki 4 + Imezimwa + Siku 1 + Saa 1 + Wiki 1 + Muda wa ujumbe wa gumzo + Muda wa ujumbe wa gumzo unaweza kuisha baada ya muda fulani. Kumbuka: Faili zinazoshirikiwa kwenye gumzo hazitafutwa kwa mmiliki, lakini hazitashirikiwa tena kwenye mazungumzo. + Imeshindwa kuleta mipangilio ya kuashiria + Kubali + Kataa + tangu%1$skatika +%2$s + Hakuna mialiko inayosubiri + Una mialiko inayosubiri + Rudi + Ruhusa kwa ufikiaji wa faili inahitajika + Chuja mazungumzo + Mtumiaji anafuata kiungio cha umma + Wewe: %1$s + Mbele + Mbele kwa... + Matunzio + Je, bado huna seva?\nBofya hapa ili kupata moja kutoka kwa mtoa huduma + Pata msimbo wa chanzo + Kundi + Mgeni + Ufikiaji wa mgeni + Haiwezi kuwezesha/kuzima ufikiaji wa mgeni. + Ruhusu wageni kushiriki kiungo cha umma ili kujiunga na mazungumzo haya. + Ruhusu wageni + Ingiza nenosiri + Nenosiri la ufikiaji wa mgeni + Hitilafu wakati wa kuweka / kulemaza nenosiri. + Weka nenosiri ili kuzuia ni nani anayeweza kutumia kiungo cha umma. + Ulinzi wa nenosiri + Tuma mialiko upya + Mialiko haikutumwa kwa sababu ya hitilafu + Mialiko ilitumwa nje tena + Shirikisha kiungio cha mazungumzo + Ingiza ujumbe... + Uboreshaji wa betri haujapuuzwa. Hii inapaswa kubadilishwa ili kuhakikisha kuwa arifa zinafanya kazi chinichini! Tafadhali bofya SAWA na uchague \"Programu zote\" -> %1$s -> Usiboresha + Puuza uboreshaji wa betri + Mazungumzo muhimu + \"Usisumbue\" hali ya mtumiaji inapuuzwa kwa mazungumzo muhimu + Wakati batili + Mialiko + Jiunge mazungumzo ya wazi + Weka + Unahitaji kutangaza msimamizi mpya kabla ya kuondoka kwenye mazungumzo + %1$s niliiboresha mwisho %2$s + Acha mazungumzo + Inaondoka kwenye simu... + GNU General Public License, Toleo la 3 + Leseni + %s kikomo cha wahusika kimepigwa + Lobi + Mkutano huu umepangwa kwa %1$s + Mkutano utaanza hivi karibuni + Sasa unasubiria katika lobi + Eneo lako la sasa + Ruhusa ya eneo inahitajika + Nafasi haijulikani + Imefungwa + Bonyeza kufungua + Haijawekwa + Weka alama kama iliyosomwa + Weka alama kama haijasomwa + Mazungumzo yametiwa alama kuwa muhimu + Mazungumzo hayajatiwa alama kuwa nyeti + Mazungumzo yametiwa alama kuwa nyeti + Mazungumzo hayajatiwa alama kuwa muhimu + Mkutano umeisha + Ujumbe umeongezwa katika kumbukumbu + Imeshindwa + Imeshindwa kutuma ujumbe + Nje ya mtandao + Ghairi jibu + Usomaji wa ujumbe + Inatuma + Ujumbe umetumwa + Maikrofoni imewashwa na sauti inarekodiwa + Ili kuwezesha mawasiliano ya sauti tafadhali toa ruhusa ya \"Mikrofoni\". + Ulikosa simu kutoka %s + Msimamizi + Mazungumzo mapya + Mwonekano + Mitajo ambayo haijasomwa + Jumbe ambazo hazijasomwa + %1$s haipatikani (haijasakinishwa au kuzuiwa na msimamizi) + Mgeni + Hapana + Hakuna mazungumzo ya wazi + Hakuna mazungumzo ya wazi ambayo unaweza kujiunga.\nAidha hakuna mazungumzo ya wazi au tayari umejiunga nayo yote. + Hakuna proksi + Huruhusiwi kuwezesha sauti! + Huruhusiwi kuwezesha video! + Si sasa + %1$sjuu ya %2$s njia ya arifu + Simu + Julisha kuhusu simu zinazoingia + Jumbe + Jilusha kuhusu jumbe zinazoingia + Vipakiwa + Julisha kuhusu mwenendo wa upakiaji + Mipangilio ya arifu + Arifa hazijawekwa vizuri + Ruhusa ya arifa na mipangilio ya betri imewekwa ipasavyo ili kupokea arifa. Iwapo una matatizo ya kupokea arifa hata hivyo, tafadhali angalia kama njia za arifa za simu na ujumbe zimewashwa. Usaidizi zaidi unaweza kupatikana katika DontKillMyApp.com au kwenye orodha ya utatuzi wa matatizo. Ikiwa hii haisaidii, tafadhali nenda kwenye skrini ya utambuzi na utume ripoti ya hitilafu. + Utatuzi wa arifa + Julisha kila wakati + Julisha inapotajwa + Usijulishe + Kwa sasa nje ya mtandao, tafadhali angalia mtandao wako + SAWA + Mkutano unaendelea + Fungua mazungumzo kwa watumiaji waliojiandikisha + Pia wazi kwa watumiaji wa programu wageni + Mmiliki + Washiriki + Ongeza washiriki + Nenosiri + Pangilia ruhusa + Ruhusa kadhaa zilikataliwa + Tafadhali ruhusu ruhusa + Mipangilio ya wazi + Tafadhali toa ruhusa katika Mipangilio > Ruhusa + Akaunti haipatikani + Piga gumzo kupitia %s + Weka kimya mikrofoni + Wezesha mikrofoni + Jumbe + Faragha + Taarifa binafsi + Pandisha cheo hadi msimamizi + Mazungumzo ya umma + Arifa zinazotumwa na programu hata wakati huitumii zimezimwa + Samahani hitilafu imetokea %1$s + Samahani, hitilafu fulani imetokea, haiwezi kuleta ujumbe wa jaribio + Arifa kutoka kwa programu imetumwa kwa mafanikio. Sasa unapaswa kupokea arifa kwenye kifaa hiki yenye kichwa \'Kujaribu arifa zinazotumwa na programu hata wakati huitumii\'. + Sukuma-kuzungumza + Huku maikrofoni ikiwa imezimwa, bofya& shikilia ili kutumia Push-to-talk + Nikumbushe baadaye + Ondoa kutoka katika pendwa + Ndoa kundi na wanachama + Ondoa washiriki + Ondoa nenosiri + Ondoa timu na wanachama + Badilisha jina la mazungumzo + Badili jina + Jibu + Jibu kwa faragha + Chumba kimehifadhiwa kikamilifu + Hifadhi + Imehifadhiwa kikamilifu + Sekunde 30 + Dakika 5 + Dakika 1 + Dakika 10 + Haraka + 600 + 60 + 30 + 300 + Tafuta + Futa utafutaji + Chagua akaunti + Sasisha ujumbe + Tuma rekodi ya sauti + Mazungumzo nyeti + Onyesho la kuchungulia la ujumbe litazimwa katika orodha ya mazungumzo na arifa + %1$sametuma GIF + Umetuma GIF + %1$sametuma video + Umetuma video + %1$sametuma sauti + Umetuma sauti + %1$sametuma picha + Umetuma picha + %1$sametuma kadi ya deck + Jaribu mtandao wa seva + Tafadhali pandisha viwango kanzidata yako %1$s + Imeshindwa kuingiza akaunti iliyochaguliwa + Kiungo cha kiolesura chako cha %1$s unapokifungua kwenye kivinjari. + Ingiza akaunti kutoka kwenye programu %1$s + Ingiza akaunti + Ingiza akaunti kutoka kwenye programu %1$s + Ingiza akaunti + Tafadhali leta %1$syako nje ya matengenezo + Tafadhali maliza usakinishaji wako %1$s + Inajaribu mtandao + Seva haijasakinisha programu ya Talk inayotumika + Anwani ya seva https://... + %1$sanafanya kazi na %2$s13 na zaidi + Pangilia nensiri jipya + Pangilia nensiri + Mipangilio + Akaunti yako iliyopo tayari imesasishwa, badala ya kuongeza mpya + a daraja la juu + Mwonekano + Simu + Tafadhali wasiliana na msimamizi wa + Fungua skrini ya utambuzi ili kuangalia mipangilio au kuunda ripoti ya hitilafu + Uchunguzi + Huagiza kibodi kuzima ujifunzaji wa kibinafsi (bila dhamana) + Kibodi fiche + Hakuna sauti + Programu ya Talk haijasakinishwa kwenye seva uliyojaribu kuthibitisha dhidi yake + Arifa + Arifa zimekataliwa + Arifa zimetolewa + Jumbe + Linganisha anwani kulingana na nambari ya simu ili kujumuisha njia ya mkato ya Talk kwenye programu ya mawasiliano ya mfumo + Hitilafu 429 maombi mengi + Unaweza kuweka nambari yako ya simu ili watumiaji wengine waweze kukupata + Ingiza namba ya simu + Namba ya simu batili + Namba ya simu imepangiliwa kikamilifu + Namba ya simu + Ujumuishaji wa nambari ya simu + Faragha + Mwenyeji wa wakala + Nenosiri la wakala + Poti ya wakala + Aina ya wakala + Jina la wakala + Shiriki hali yangu ya kusoma na uonyeshe hali ya usomaji ya wengine + Hali ya kusoma + Idhinisha upya akaunti + Ondoa + Ondoa akaunti + Tafadhali thibitisha nia yako ya kuondoa akaunti ya sasa. + Funga %1$s ukitumia mbinu ya kufunga skrini ya Android au mbinu ya kibayometriki inayotumika + Muda wa kutotumika kwa kufunga skrini umekwisha + Funga skrini + Huzuia picha za skrini katika orodha ya hivi majuzi na ndani ya programu + Ulinzi wa skrini + Toleo la seva ni la zamani sana na halitatumika katika toleo lijalo! + Toleo la seva ni zee na halitumiki katika toleo hili la programu ya Android + Seva isiyotumika + Programu ya arifa za seva haijasakinishwa + Imewekwa na kiokoa betri + Giza + Tumia chaguomsingi ya mfumo + Lengo + Mwanga + Mandhari + Shiriki hali yangu ya uchapaji na uonyeshe hali ya uchapaji ya wengine + Hali ya kuandika inapatikana tu unapotumia hali ya nyuma ya utendaji wa juu (HPB) + Hali ya uchapaji + Seva mbadala inahitaji kitambulisho + Onyo + Akaunti ya sasa pekee ndiyo inaweza kuidhinishwa tena + Shirikisha mawasiliano + Ruhusa ya kusoma anwani inahitajika + Shirikisha eneo la sasa + Shirikisha kiungo + Shirikisha eneo + Shirikisha eneo hili + Chagua akaunti + Vipengele vilivyoshirikiwa + Kadi ya deck + Picha, faili,jumbe za sauti + Hakuna vipengele vilivyoshirikiwa + Mahali/eneo + Eneo lililoshirikishwa + Wakati arifa hazijawekwa vizuri, onyesha onyo la kawaida + Onyesha onyo la arifa za kawaida + Panga kwa + Anza gumzo la kundi + Muda wa kuanza + Badili akaunti + Timu + Jaribu arifa ya kushinikiza + Jaribu matokeo + Leo katika %1$s + Kesho katika %1$s + Chagua faili + Tuma faili hizi katka %1$s? + Tuma faili hili katika %1$s? + Samahani, upakiaji umeshindikana + Imeshindwa kupakia %1$s + Kushindwa + Shirikisha kutoka %1$s + Pakia kutoka katika kifaa + Inapakia + %1$skwenda %2$s-%3$s/%% + Chukua picha + Chukua video + Mtumiaji + Video inarekodi kutoka %1$s + Talk inarekodi kutoka %1$s (%2$s) + Shikilia kurekodi, achia kutuma + Ruhusa kwa kurekodi sauti inahitajika + Telezesha kughairi + Webinar + Ndiyo + Wiki ijayo + Hakuna mazungumzo yaliyohifadhiwa kwenye kumbukumbu + Hakuna jumbe za nje ya mtandao zilizohifadhiwa + Hakuna muunganisho wa nambari ya simu kwa sababu ya kukosa ruhusa + Jumbe zote + \@-mitajo pekee + Imezimwa + Chaguo msingi + Fuata mipangilio ya mazungumzo + Saa 1 + Mtandaoni + Hadhi ya mtandaoni + Mazungumzo ya wazi + Fungua katika faili za programu + Nukuu za wazi + Nenda kwenye mjadala + Cheza/simamisha ujumbe wa sauti + Udhibiti wa kasi ya mshindonyuma + Ongeza mbadala + Hariri kura + Maliza kura ya maoni + Je, kweli ungependa kukomesha kura hii ya maoni? Hili haliwezi kutenduliwa. + Huwezi kupiga kura ukitumia machaguo zaidi ya kura hii. + Majibu mengi + Futa mbadala %1$d + Chaguo %1$d + Machaguo + Kura ya maoni ya kibinafsi + Swali + Swali lako + Matokeo + Mipangilio + Kura + Kura imewasilishwa + Imepangiliwa mwanzo + Msimbo wa QR haukuweza kusomeka + Inua mkono + Zote + Kushiriki faili kutoka kwa hifadhi hakuwezekani bila ruhusa + Mijadala ya hivi karibuni + Simu inarekodiwa + Ghairi kuanza kurekodi + Imeshindwa kurekodi. Tafadhali wasiliana na msimamizi wako. + Anza kurekodi + Je, kweli unataka kusimamisha kurekodi? + Simamisha kurekodi simu + Simamisha kurekodi + Imesimama kurekodi + Idhini ya kurekodi inahitajika kwa simu zote + Rekodi inaweza kujumuisha sauti yako, video kutoka kwa kamera na kushiriki skrini. Idhini yako inahitajika kabla ya kujiunga kwenye simu. Je, unakubali? + Inahitaji idhini ya kurekodi kabla ya kujiunga na simu kwenye mazungumzo haya + Idhini ya kurekodi + Simu inaweza kuwa imerekodiwa + Inarekodi + Mazungumzo yaliyoondolewa %1$s kutoka katika vipendwa + Mazungumzo %1$syalibadilishwa jina + Tuma upya + Pangilia hali + Haiwezekani kujiunga na vyumba vingine ukiwa kwenye simu + Hifadhi + Changanua Msimbo wa QR + Sawazisha kwa seva zinazoaminika pekee + Shirikisho + Inaonekana kwa watu katika tukio hili na wageni pekee + Kawaida + Inaonekana tu kwa watu wanaolinganishwa kupitia ujumuishaji wa nambari ya simu kupitia Talk katika simu ya mkononi + Binafsi + Sawazisha kwa seva zinazoaminika na kitabu cha anwani cha kimataifa na cha umma + Imechapishwa + Geuza upeo + Badili kiwango cha faragha ya %1$s + Tembeza hadi chini + Tafuta ikoni + sukunde zilizopita + Iliyochaguliwa + Tuma barua pepe + Tuma kwa + Huruhusiwi kushirikisha maudhui katika gumzo hii + Tuma kwa... + Tuma bila arifa + Imetumwa + Pangilia avata kutoka katika kamera + Panglia hali + Pangilia hali ya ujumbe + Shirikisha + Jiunge mazungumzo %1$s katika %2$s + Sauti + Faili + Midia + Mengine + Kura ya maoni + Kurekodi simu + Sauti + Onesha sababu ya zuio + Onesha washiriki waliozuiliwa + Kipendwa + Huruhusiwi kuanzisha simu + Unda mjadala + Umeanzisha simu + Ujumbe wa hadhi + Hali Imerejeshwa + Badili hadi chumba cha mapumziko + Badili hadi chumba kikuu + Chukua picha + Hitilafu wakati wa kuchukua picha + Kupiga picha hakuwezekani bila ruhusa + Chukua picha tena + Tuma + Badili kamera + Dondosha picha + Punguza ukubwa wa picha + Washa tochi + Dakika 30 + Wiki hii + Huu ni ujumbe wa jaribio + Wikendi hii + Ghairi uundaji wa mjadala + Arifa za mjadala + Jibu + Kichwa cha mjadala + Hakuna nyuzi zilizopatikana + Leo + Kesho + Tafsiri + Utafsiri + Nakili maandishi yaliyotafsiriwa + Lugha iliyotambuliwa + Mipangilio ya kifaa + Haikuweza kugundua lugha + Utafsiri umeshindikana + Tangu/ kutoka + Mpaka/ hadi + na mwingine 1 anachapisha + wanachapisha + anachapisha + na %1$s wengine wanachapisha + Ondoa mazungumzo kwenye kumbukumbu + Mazungumzo yakishatolewa kwenye kumbukumbu, yataonyeshwa kwa chaguomsingi tena. + Haijahifadhiwa %1$s + Batilisha marufuku + Haijasomwa + Pakia avata mpya kutoka kwenye kifaa + %1$s nje ya ofisi na inaweza isijibu + %1$s nje ya ofisi leo + Mbadala + Tumia avata + Anwani + Jina kamili + Barua pepe + Namba ya simu + Twitter + Wavuti + Wadhifa + Imeshindwa kupata maelezo binafsi ya mtumiaji. + Hakuna maelezo binafsi yaliyowekwa + Ongeza jina, picha na maelezo ya mawasiliano kwenye ukurasa wako wa wasifu. + Simu ya video + Hadhi yako ni nini? + + See %d similar message + Ona %d jumbe sawasawa + + + This conversation will be automatically deleted for everyone in %1$d day of no activity + Mazungumzo haya yatafutwa kiotomatiki kwa kila mtu baada ya siku %1$d bila shughuli yoyote + + + %d reply + %dmajibu + + + %d vote + %d kura + + diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml new file mode 100644 index 0000000..3b79945 --- /dev/null +++ b/app/src/main/res/values-sw600dp/dimens.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000..d39f16b --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,727 @@ + + + Düzenle + Ekle + Notlar uygulamasına ekle + %1$s görüşmesi sık kullanılara eklendi + %s içinde ara + Çevrim dışı görün + Görüşmeyi arşivle + Bir görüşme arşivlendiğinde, varsayılan olarak gizlenir. Arşivlenmiş görüşmeleri görüntülemek için \"Arşivlenmiş\" süzgecini seçin. Doğrudan anmalar yine de alınır. + Arşivlenmiş + %1$s arşivlendi + Sesli çağrı + Bluetooth + Ses çıkışı + Telefon + Hoparlör + Kablolu kulaklık + Durumunuz otomatik olarak ayarlanmış + Avatar + Uzakta + Geri düğmesi + Yasakla + Katılımcıyı yasakla + Yasaklama listesi + Meşgul + Takvim + Gelişmiş çağrı seçenekleri + Çağrı bir saattir sürüyor. + Bildirim olmadan çağrı + Kamera izinleri verildi. Lütfen kamerayı yeniden seçin. + Oturum açmaktan vazgeç + Buluttan avatar seç + Durum iletisini kaldır + Durum iletisinin kaldırılma süresi + Kapat + Kapatma simgesi + Bağlantı kuruldu + Sunucu ile bağlantı yok + Bağlantı kesildi. Gönderilen iletiler kuyruğa alındı + Sesli iletinin sürekli olarak kaydedilmesi için kaydı kilitleyin + Görüşme arşivlendi + Görüşme salt okunur + Görüşme salt okunur olarak ayarlanamadı + Görüşmeler + Görüşme oluştur + Sorun ekle + Özel + Tehlikeli bölge + %1$s, %2$s içinde + Avatarı sil + Ses kaydını sil + %1$s görüşmesi silindi + Rahatsız etmeyin + Kaldırılmasın + Düzenle + 24 saatten eski iletiler düzenlenemez + İletiyi düzenle + Son kullanılan + Şifrelenmiş + Çağrıyı sonlandır + Çağrıyı herkes için sonlandır + Çizelgeleriniz yüklenirken bir sorun çıktı + Katılımcının yasaklaması kaldırılırken sorun çıktı + %1$s kaydedilemedi + 15 dakika + klasör + Yükleniyor… + %1$s (%2$d) + Takip edilen yazışmalar + 4 saat + Bekleyen davetler alınamadı + (düzenlendi) + İç not + Görünmez + Diller alınamadı + Alınamadı + Bugün daha sonra + Çağrıdan ayrıl + %1$s görüşmesinden çıktınız + Diğer sonuçları yükle + Yerel zaman: %1$s + Konum izni reddedildi + Lütfen Ayarlar içinden izin verin + Konum hizmetleri kapalı + Bu özelliği kullanabilmek için konum hizmetlerini (GPS) açın + Görüşmeyi kilitle + Kilit simgesi + Eli indir + %1$s görüşmesi okunmuş olarak işaretlendi + %1$s görüşmesi okunmamış olarak işaretlendi + Anılmış + Yeniden eskiye + Eskiden yeniye + A - Z + Z - A + Büyükten küçüğe + Küçükten büyüğe + İleti kopyalandı + Bu iletiyi silmek istediğinize emin misiniz? + İleti sizin tarafınızdan silindi + %1$s tarafından düzenlendi + Anketi açmak için dokunun + Aramadan bir sonuç alınamadı + Aramak için yazmaya başlayın… + Arama… + İletiler + Tüm bildirimleri kapat + Seçilmiş hesap içe aktarıldı ve kullanılabilir + Hakkında + Etkin kullanıcı + Hesap ekle + Hesap silinmek üzere zamanlandı ve üzerinde işlem yapılamaz + Ana menüyü aç + Dosya ekle + Emoji ekle + Görüşmeye ekle + Katılımcı ekle + Sık kullanılanlara ekle + Hepsi tamam! + Pin: %1$s + %1$s kilidini aç + Bluetooth hoparlörleri açmak için lütfen \"Yakındaki aygıtlar\" iznini verin. + Görüntülü çağrı olarak yanıtla + Yalnızca sesli çağrı olarak yanıtla + Ses çıkışını değiştir + Kamerayı aç/kapat + Sonlandır + Mikrofonu aç/kapat + Resim içinde resim kipini aç + Benim görüntüme geç + GELEN + Görüşme adı + Çağrı bildirimleri + %1$s elini kaldırdı + Bağlantı yeniden kuruluyor… + ÇALIYOR + Çağrıda %1$s kişi var + %1$s kişi telefon ile + %1$s kişi görüntü ile + 45 saniye içinde yanıt alınamadı, yeniden denemek için dokunun + %s çağrı + %s görüntülü çağrı + %s sesli çağrı + Görüntülü iletişim kurabilmek için \"Kamera\" için erişme izni verin. + İptal + Yeterlilikler alınamadı, vazgeçiliyor + Alt yazı + Şimdiye kadar bilinmeyen, %1$s tarafından %2$s için yayınlanmış %3$s ile %4$s tarihleri arasında geçerli olan sertifikaya güvenilsin mi? + Sertifikayı denetleyin + SSL kurulumunuz bağlantı kurulmasını engelliyor + Kimlik doğrulama sertifikasını değiştir + Parolayı değiştir + Düzenlemeyi iptal et + Düzenlemeyi iptal et + Tüm iletileri sil + Tüm iletiler silindi + Bu görüşmedeki tüm iletileri silmek istediğinize emin misiniz? + İstemci sertifikasını değiştir + İstemci sertifikası kurulumu + ve + Kopyala + Panoya kopyalandı + Ekle + Kapalı + Yok say + Ne yazık ki bir sorun çıktı! + Diğer seçenekler + Ayarla + Atla + Bilinmiyor + Kimlik doğrulama sertifikasını seçin + Bağlantı kuruluyor … + Tamam + Görüşme açıklaması + Görüşme bilgileri + Görüntülü çağrı + Sesli çağrı + Görüşme bulunamadı + Görüşme ayarları + Bir görüşmeye katılın ya da yeni bir görüşme başlatın + Tanıdık ve çalışma arkadaşlarınıza selam verin! + Kopyala + Yeni bir görüşme oluştur + Anket ekle + Siz: + Bugün + Dün + Sil + Tümünü sil + Görüşmeyi sil + Görüşmeyi silerseniz tüm diğer katılımcılar için de silinecek. + İletiyi sil + İleti silindi ancak başka hizmetlere aktarılmış olabilir + Şimdi sil + %1$s kullanıcısı kaldırıldı + Sorumluluktan çıkar + Ses iletisi kaydet + İleti gönder + Geçerli hesap + Sunucu + Sunucu bildirimi uygulaması kurulmuş mu? + Kullanıcı adı + Kullanıcı durumu kullanıma alınmış mı? + Android sürümü + Uygulama + Uygulama adı + Kayıtlı kullanıcılar + Uygulama sürümü + Pil iyileştirmesi yok sayılmış. Her şey yolunda + Pil iyileştirmesi açık ve bu sorunlara neden olabilir. Pil iyileştirmesini kapatmalısınız! + Pil ayarları + Aygıt + Sorun çözme kontrol listesini aç + Tanılama ekranını aç + dontkillmyapp.com sitesini aç + Son firebase anında bildirim kodu alma işlemi + Son firebase anında bildirim kodu oluşturma işlemi + Herhangi bir firebase anında bildirim kodu ayarlanmamış. Lütfen bir hata bildirimi oluşturun. + Firebase anında bildirim kodu + Google Play hizmetleri kullanılamıyor. Bildirimler desteklenmiyor + Google Play hizmetleri + Google Play hizmetleri kullanılabilir + Anında bildirim vekil sunucusundaki son anında bildirim kaydı + Henüz anında bildirim vekil sunucusuna kaydedilmemiş + Sunucudaki son anında bildirim kaydı + Henüz sunucuda kaydedilmemiş + Üst veri bilgileri + Sistem raporu hazırlama + Çağrı bildirimi kanalı kullanıma alınmış mı? + İleti bildirimi kanalı kullanıma alınmış mı? + Bildirim izinleri + Telefon + Sunucudaki Konuş sürümü + Sunucu sürümü + Dış + İç + Signaling kipi + Parola geçersiz + Şu anda sunucuda bakım yapılıyor. + Uygulama sürümü çok eski + Uygulama çok eski ve artık bu sunucu tarafından desteklenmiyor. Lütfen güncelleyin. + Güncelle + Bu hesabı yeniden kullanıma almak ya da silmek ister misiniz? + Bu ortamı depolama alanına kaydetmek, aygıtınızdaki diğer uygulamaların da buna erişmesine izin verir. + İlerlemek istiyor musunuz? + Hayır + Depolama alanına kaydedilsin mi? + Evet + Görüntülenecek ad alınamadı, vazgeçiliyor + Görüntülenecek ad kaydedilemedi, vazgeçiliyor + Düzenle + Düzenle + İletiyi düzenle + Yönetici tarafından düzenlendi + Etkinlik görüşmesi menüsü + Zamanlama + 8 saat + 4 hafta + Kapalı + 1 gün + 1 saat + 1 hafta + Süreli sohbet iletileri + Sohbet iletileri süresi belirli bir süre sonra dolacak şekilde ayarlanabilir. Not: Süresi dolan iletlerdeki dosyalar sahibi için silinmez, ancak artık görüşmede paylaşılmaz. + İşaretleşme ayarları alınamadı + Kabul et + Reddet + %1$s ile %2$s arasında + Bekleyen bir davet yok + Bekleyen davetleriniz var + Geri + Dosyalara erişme izni gereklidir + Görüşmeleri süz + Herkese açık bir bağlantıyı izleyen kullanıcı + Siz: %1$s + İlet + Şuraya ilet … + Galeri + Henüz bir sunucunuz yok mu?\Listeden bir hizmet sağlayıcısı seçebilirsiniz + Kaynak kodu alın + Grup + Konuk + Konuk erişimi + Konuk erişimi izni verilemez ya da kaldırılamaz. + Konukların bu görüşmeyi herkese açık bir katılma bağlantısı ile paylaşabilmesini sağlar. + Konuklar katılabilsin + Bir parola yazın + Konuk erişimi parolası + Parola ayarlanırken ya da kaldırılırken sorun çıktı. + Herkese açık bağlantıya erişimi sınırlamak için bir parola belirleyin. + Parola koruması + Davetleri yeniden gönder + Bir sorun nedeniyle davetler gönderilemedi. + Davetler gönderildi. + Görüşme bağlantısını kopyala + Bir ileti yazın… + Pil iyileştirmesi yok sayılmıyor. Bildirimlerin arka planda çalıştığından emin olmak için bu durum değiştirilmelidir! Lütfen Tamam üzerine tıklayın ve \"Tüm uygulamalar\" -> %1$s -> İyileştirilmesin seçeneğini seçin + Pil iyileştirmesi yok sayılsın + Önemli görüşme + Önemli görüşmeler için \"Rahatsız etmeyin\" kullanıcı durumu yok sayılır + Zaman geçersiz + Davetler + Açık görüşmelere katıl + Tut + Görüşmeden çıkmadan önce başka bir kullanıcıyı sorumluluğa yükseltmelisiniz + %1$s | Son değişiklik: %2$s + Görüşmeden çık + Çağrıdan çıkılıyor… + GNU Genel Kamu Lisansı, 3. sürüm + Lisans + %s karakter sınırına ulaşıldı + Giriş + Bu toplantı %1$s zamanına ayarlanmış + Toplantı yakında başlayacak + Şu anda girişte bekliyorsunuz + Geçerli konumunuz + konum izinleri gerekli + Konum bilinmiyor + Kilitli + Kilidi açmak için dokunun + Ayarlanmamış + Okunmuş olarak işaretle + Okunmamış olarak işaretle + Görüşme önemli olarak işaretlendi + Görüşmenin ciddi işareti kaldırıldı + Görüşme ciddi olarak işaretlendi + Görüşmenin önemli işareti kaldırıldı + Toplantı sona erdi + İleti notlara eklendi + Tamamlanamadı + İleti gönderilemedi. + Çevrim dışı + Yanıtı iptal et + İleti okundu + Gönderiliyor + İleti gönderildi + Mikrofon açık ve ses kaydediliyor + Sesli iletişim kurabilmek için \"Mikrofon\" için erişme izni verin. + %s sizi aramış + Sorumlu + Yeni görüşme + Görünürlük + Okunmamış anmalar + Okunmamış iletiler + %1$s kullanılamıyor (kurulmamış ya da yönetici tarafından engellenmiş) + Konuk + Hayır + Herhangi bir açık görüşme yok + Katılabileceğiniz açık bir görüşme yok.\nHerhangi bir açık görüşme olmayabilir ya da zaten tüm görüşmelere katılmış olabilirsiniz. + Vekil sunucu yok + Sesi açma izniniz yok! + Görüntüyü açma izniniz yok! + Şimdi değil + %1$s %2$s bildiri kanalında + Çağrılar + Gelen çağrılar bildirilsin + İletiler + Gelen iletiler bildirilsin + Yüklemeler + Yükleme ilerlemesi bildirilsin + Bildirim ayarları + Bildirimler doğru olarak ayarlanmamış + Bildirim izni ve pil ayarları, bildirimleri almak için doğru şekilde ayarlanmış. Yine de bildirim almakta sorun yaşıyorsanız lütfen çağrı ve iletiler için bildirim kanallarının kullanıma alınmış olup olmadığını denetleyin. Daha fazla yardım almak için DontKillMyApp.com adresine ya da sorun giderme kontrol listesine bakabilirsiniz. Bunlar işe yaramazsa lütfen tanılama ekranına gidin ve bir hata bildirimi gönderin. + Bildirim sorunlarını çözme + Her zaman bildirilsin + Anmalar bildirilsin + Asla bildirilmesin + Şu anda çevrim dışı, lütfen bağlantınızı denetleyin + Tamam + Toplantı sürüyor + Görüşmeyi kayıtlı kullanıcılara aç + Ayrıca konuk uygulama kullanıcılarına da açılsın + Sahip + Katılımcılar + Katılımcı ekle + Parola + İzinleri ayarla + Bazı izinler verilmemiş. + Lütfen izinleri verin + Ayarları aç + Lütfen Ayarlar > İzinler bölümündeki izinleri verin + Hesap bulunamadı + %s ile sohbet + Mikrofonu kapat + Mikrofonu aç + İletiler + Gizlilik + Kişisel bilgiler + Sorumluluğa yükselt + Herkese açık görüşme + Anında bildirimler kapalı + Ne yazık ki bir sorun çıktı. Hata: %1$s + Ne yazık ki bir sorun çıktı. Deneme anında bildirimi alınamadı + Anında bildirim gönderildi. Bu aygıtta \'Anlık bildirimler denemesi\' konulu bir bildirim almalısınız + Bas konuş + Mikrofon kapalıyken, Bas-konuş üzerine tıklayıp basılı tutun + Sonra hatırlat + Sık kullanılanlardan kaldır + Grup ve üyelerini sil + Katılımcıyı çıkar + Parolayı kaldır + Takımı ve üyelerini kaldır + Görüşmeyi yeniden adlandır + Yeniden adlandır + Yanıtla + Kişisel yanıt gönder + Oda tutuldu + Kaydet + Kaydedildi + 30 saniye + 5 dakika + 1 dakika + 10 dakika + Hemen + 600 + 60 + 30 + 300 + Arama + Aramayı temizle + Bir hesap seçin + İletiyi güncelle + Ses kaydını gönder + Ciddi görüşme + Görüşme listesi ve bildirimlerde ileti ön izlemesi kapatılacak + %1$s bir GIF gönderdi. + Bir GIF gönderdiniz. + %1$s bir görüntü gönderdi. + Bir görüntü gönderdiniz. + %1$s bir ses gönderdi. + Bir ses gönderdiniz. + %1$s bir görsel gönderdi. + Bir görsel gönderdiniz. + %1$s bir tahta kartı gönderdi + Sunucu bağlantısını sına + Lütfen %1$s veri tabanınızı güncelleyin + Seçilmiş hesap içe aktarılamadı + %1$s site arayüzü için tarayıcıda açacağınız bağlantı. + %1$s uygulamasındaki hesabı içe aktar + Hesabı içe aktar + %1$s uygulamasındaki hesapları içe aktar + Hesapları içe aktar + Lütfen %1$s kopyanızı bakım kipinden çıkarın + Lütfen %1$s kurulumunuzu tamamlayın + Bağlantı sınanıyor + Sunucuda desteklenen bir Konuş uygulaması kurulu değil + Sunucu adresi https://… + %1$s yalnızca %2$s 13 ve üzerinde çalışır + Yeni parola ayarla + Parola ayarla + Ayarlar + Zaten bir hesabınız bulunduğundan yeni hesap eklenmesi yerine geçerli hesap güncellendi + Gelişmiş + Görünüm + Çağrılar + Lütfen şuranın yöneticisi ile görüşün + Ayarları denetlemek için tanılama ekranını açın ya da bir hata bildirimi oluşturun + Tanılama + Klavyede kişisel öğrenmeyi kapatır (garanti edilmez) + Tuş takımı gizliliği + Ses yok + Kimlik doğrulaması yapmak istediğiniz sunucu üzerinde Konuş uygulaması kurulu değil + Bildirimler + Bildirimler reddediliyor + Bildirimlere izin veriliyor + İletiler + Kişileri telefon numaralarına göre eşleştirerek sistem kişiler uygulamasında Konuş kısa yolunu görüntüler + Hata 429 çok fazla sayıda istek yapıldı + Telefon numaranızı ayarlayarak diğer kullanıcıların sizi bulmasını sağlayabilirsiniz + Telefon numarasını yazın + Telefon numarası geçersiz + Telefon numarası ayarlandı + Telefon numarası + Telefon numarası bütünleştirmesi + Gizlilik + Vekil sunucu adı + Vekil sunucu parolası + Vekil sunucu bağlantı noktası + Vekil sunucu türü + Vekil sunucu kullanıcı adı + Okundu durumumu paylaş ve diğerlerinin okundu durumunu görüntüle + Okundu durumu + Hesaba yeniden izin ver + Sil + Hesabı sil + Lütfen geçerli hesabı silmek istediğinizi onaylayın. + %1$s Android ekran kilidi ya da desteklenen biyometrik yöntem ile kilitlensin + Ekran kilidi zaman aşımı süresi + Ekran kilidi + Son dosyalar listesi ve uygulama ekran görüntülerinin alınmasını engeller + Ekran güvenliği + Sunucu sürümü çok eski ve gelecek sürümde artık desteklenmeyecek + Sunucu sürümü çok eski ve bu Android uygulaması tarafından desteklenmiyor + Sunucu desteklenmiyor + Sunucu bildirimleri uygulaması kurulmamış + Pil koruyucu tarafından ayarlandı + Koyu + Sistem varsayılanı kullanılsın + tema + Açık + Tema + Yazıyor durumum paylaşılsın ve diğerlerinin yazıyor durumu görüntülensin + Yazma durumu yalnızca yüksek başarımlı arka yüz (HPB) üzerinde kullanılabilir + Yazma durumu + Vekil sunucu için kimlik doğrulaması gerekli + Uyarı + Yalnızca geçerli hesaba yeniden izin verilebilir + Kişiyi paylaş + Kişileri okuma izni gerekiyor + Geçerli konumu paylaş + Bağlantıyı paylaş + Konumu paylaş + Bu konumu paylaş + Hesap seçin + Paylaşılmış ögeler + Tahta kartı + Görseller, dosyalar, ses iletileri… + Paylaşılmış bir öge yok + Konum + Paylaşılan konum + Bildirimler doğru olarak ayarlanmamışsa standart uyarı görüntülensin + Standart uyarı bildirimi görüntülensin + Sıralama + Grup sohbeti başlat + Başlangıç zamanı + Hesabı değiştir + Takım + Anında bildirim denemesi + Deneme sonuçları + Bugün %1$s zamanında + Yarın %1$s zamanında + Dosyaları seçin + Bu dosyalar %1$s alıcısına gönderilsin mi? + Bu dosya %1$s alıcısına gönderilsin mi? + Yüklenemedi + %1$s yüklenemedi + Sorun çıktı + %1$s üzerinden paylaş + Aygıttan yükle + Yükleniyor + %1$s ile %2$s - %3$s\%% + Fotoğraf çek + Görüntü al + Kullanıcı + %1$s üzerinden görüntü kaydı + %1$s Konuş kaydı (%2$s) + Kaydetmek için basılı tutun, bırakarak gönderin. + Ses kaydetme izninin verilmesi gereklidir + « İptal etmek için kaydırın + İnternet sunumu + Evet + Sonraki hafta + Arşivlenmiş bir görüşme yok + Herhangi bir çevrim dışı iletisi kaydedilmemiş + İzinler eksik olduğundan telefon numarası bütünleştirmesi yok + Tüm iletiler + Yalnızca @-anmaları + Kapalı + Varsayılan + Görüşme ayarları kullanılsın + 1 saat + Çevrim içi + Çevrim içi durumu + Açık görüşmeler + Dosyalar uygulamasında aç + Notları aç + Yazışmaya git + Ses iletisini oynat/duraklat + Oynatma hızı denetimi + Seçenek ekle + Oyu düzenle + Anleti sonlandır + Bu anketi sonlandırmak istediğinize emin misiniz? Bu işlem geri alınamaz! + Bu ankete başka seçenekler ile oy veremezsiniz. + Birden çok yanıt + %1$d seçeneğini sil + %1$d seçeneği + Seçenekler + Kapalı anket + Soru + Sorunuz + Sonuçlar + Ayarlar + Oy + Oy verildi + Önceden ayarlanmış + Kare kod okunamadı + El kaldır + Tümü + İzin verilmeden depolamadaki dosyalar paylaşılamaz + Son yazışmalar + Çağrı kaydediliyor + Kaydı başlatmaktan vazgeç + Kayıt yapılamadı. lütfen yöneticiniz ile görüşün. + Kaydı başlat + Kaydı durdurmak istediğinize emin misiniz? + Çağrı kaydını durdur + Kaydı durdur + Kayıt durduruluyor… + Tüm çağrılar için kayıt alma rızası istensin + Kayıtta sesiniz, kamera görüntünüz ve ekran paylaşımlarınız bulunabilir. Çağrıya katılmadan önce kayıt alma rızası vermeniz gerekir. Rıza veriyor musunuz? + Bu görüşmedeki çağrılara katılmadan önce kayıt alma rızası istensin + Kayıt alma rızası + Çağrının kaydı alınabilir. + Kaydediliyor + %1$s görüşmesi sık kullanılanlardan kaldırıldı + %1$s görüşmesi yeniden adlandırıldı + Yeniden gönder + Durumu sıfırla + Bir çağrı sürerken diğer odalara katılamazsınız + Kaydet + Kare kodu tara + Yalnızca güvenilen sunucular ile eşitlensin + Birleşik + Yalnızca bu kopyadaki kişiler ve konuklar görebilir + Yerel + Yalnızca mobil aygıt üzerinde Konuş telefon numarası bütünleştirmesi ile eşleşen kişiler görebilir + Kişisel + Genel ve herkese açık adres defteri ile ve güvenilen sunucularla eşitlensin + Yayınlanmış + Ölçeği değiştir + %1$s gizlilik düzeyini değiştir + Aşağıya kaydır + Arama simgesi + saniye önce + Seçilmiş + E-posta gönder + Şuraya gönder + Bu sohbette içerik paylaşma izniniz yok + Şuraya gönder … + Bildirim olmadan gönder + Ayarla + Avatarı kamerayla ayarla + Durumu ayarla + Durum iletisini ayarla + Paylaş + %2$s üzerindeki%1$s görüşmesine katıl + Ses + Dosya + Ortam + Diğer + Anket + Çağrı kaydı + Ses + Yasaklama nedeni görüntülensin + Yasaklanmış katılımcıları görüntüle + Sık kullanılanlara ekle + Bir çağrı başlatma izniniz yok + Bir yazışma oluştur + bir çağrı başlattı + Durum iletisi + Durum geri alındı + Çalışma odasına geç + Ana odaya geç + Bir fotoğraf çekin + Fotoğraf çekilirken sorun çıktı + İzin verilmeden bir fotoğraf çekilemez + Fotoğrafı yeniden çek + Gönder + Kamerayı değiştir + Fotoğrafı kırp + Görsel boyutunu küçült + El fenerini aç/kapat + 30 dakika + Bu hafta + Bu bir deneme iletisidir + Bu hafta sonu + Yazışma başlatmaktan vazgeç + Yazışma bildirimleri + Yanıtla + Yazışma başlığı + Herhangi bir yazışma bulunamadı + Bugün + Yarın + Çevir + Çeviri + Çevrilmiş yazıyı kopyala + Dili algıla + Aygıt ayarları + Dil algılanamadı + Çeviri yapılamadı + Kaynak dil + Hedef dil + ve 1 diğer kişi yazıyor… + kişi yazıyor… + yazıyor… + ve %1$s diğer kişi yazıyor… + Görüşmeyi arşivden çıkar + Bir görüşme arşivden çıkarıldığında varsayılan olarak yeniden görüntülenir. + %1$s arşivden çıkarıldı + Yasaklamayı kaldır + Okunmamış + Aygıttan yeni avatar yükle + %1$s iş yeri dışında ve yanıt veremeyebilir. + %1$s bugün iş yeri dışında + Yedek: + Kullanıcı avatarı + Adres + Tam ad + E-posta + Telefon numarası + Twitter + Site + Durum + Kullanıcının kişisel bilgileri alınamadı + Herhangi bir kişisel bilgi ayarlanmamış + Profil sayfanızdan ad, görsel ve iletişim bilgilerinizi ekleyin. + Görüntülü çağrı + Durumunuz nedir? + + %d benzer iletiye bakın + %d benzer iletiye bakın + + + Bu görüşme, %1$d gün boyunca etkileşim olmazsa herkesten otomatik olarak silinecek. + Bu görüşme, %1$d gün boyunca etkileşim olmazsa herkesten otomatik olarak silinecek. + + + %d yanıt + %d yanıt + + + %d oy + %d oy + + diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml new file mode 100644 index 0000000..d367fed --- /dev/null +++ b/app/src/main/res/values-ug/strings.xml @@ -0,0 +1,642 @@ + + + تەھرىر + قوش + ئىزاھاتقا قوشۇڭ + ياقتۇرىدىغانلارغا %1$s پاراڭ قوشۇلدى + %s دىن ئىزدەڭ + تورسىز كۆرۈنۈش + ئارخىپ سۆھبىتى + ئارخىپلاشتۇرۇلغان + كۆك چىش + ئاۋاز چىقىرىش + تېلېفون + سۆزلىگۈچى + سىملىق تىڭشىغۇچ + ھالىتىڭىز ئاپتوماتىك تەڭشەلدى + Avatar + يىراق + قايتىش كۇنۇپكىسى + Ban + قاتناشقۇچىنى چەكلەش + چەكلەش تىزىملىكى + ئالدىراش + كالېندار + ئىلغار چاقىرىش تاللانمىلىرى + تېلېفون بىر سائەت داۋاملاشتى. + ئۇقتۇرۇش قىلماي تېلېفون قىلىڭ + كامېرا ئىجازەتنامىسى بېرىلگەن. كامېرانى قايتا تاللاڭ. + كىرىشنى ئەمەلدىن قالدۇرۇڭ + بۇلۇتتىن باش سۈرىتىنى تاللاڭ + ھالەت ئۇچۇرىنى تازىلاش + كېيىنكى ھالەت ئۇچۇرىنى تازىلاڭ + تاقاش + سىنبەلگە تاقاش + ئۇلىنىش قۇرۇلدى + ئۇلىنىش يوقاپ كەتتى - ئەۋەتىلگەن ئۇچۇرلار ئۆچرەتتە تۇرىدۇ + ئاۋاز ئۇچۇرىنى ئۇدا خاتىرىلەش ئۈچۈن قۇلۇپ خاتىرىلەش + سۆھبەت پەقەت ئوقۇلىدۇ + سۆھبەت + سۆھبەت قۇرۇش + مەسىلە پەيدا قىلىڭ + Custom + خەتەر رايونى + %2$s دىكى%1$s + باش سۈرىتىنى ئۆچۈرۈڭ + ئۆچۈرۈلگەن سۆھبەت%1$s + ئاۋارە قىلماڭ + ئېنىق ئەمەس + تەھرىر + ئۇچۇرنى تەھرىرلەش + يېقىنقى + شىفىرلانغان + ئاخىرلىشىش تېلېفونى + ھەممەيلەنگە تېلېفون قىلىش + پاراڭلىرىڭىزنى يۈكلەشتە مەسىلە كۆرۈلدى + قاتناشقۇچىنى چەكلىگەندە خاتالىق كۆرۈلدى + %1$s نى تېجەلمىدى + 15 مىنۇت + ھۆججەت قىسقۇچ + Loading… + %1$s (%2$d) + 4 سائەت + ساقلىنىۋاتقان تەكلىپلەرنى ئالالمىدى + (تەھرىرلەندى) + ئىچكى خاتىرە + كۆرۈنمەيدۇ + تىللارنى ئەسلىگە كەلتۈرگىلى بولمىدى + ئەسلىگە كەلتۈرۈش مەغلۇب بولدى + بۈگۈن بۈگۈن + تېلېفون قىلىڭ + You left the conversation %1$s + تېخىمۇ كۆپ نەتىجىلەرنى يۈكلەڭ + سۆھبەتنى قۇلۇپلاڭ + قۇلۇپ بەلگىسى + تۆۋەن قول + بەلگە قىلىنغان سۆھبەت%1$s دەپ يېزىلغان + %1$s ئوقۇلمىغان دەپ بەلگە قويۇلغان + تىلغا ئېلىنغان + ئەڭ يېڭى + ئەڭ كونا + A - Z. + Z - A. + ئەڭ چوڭ + ئەڭ كىچىك + سىز ئۆچۈرگەن ئۇچۇر + %1$s تەھرىرلىگەن + بېلەت تاشلاشنى چېكىڭ + ئىزدەش نەتىجىسى يوق + ئىزدەشنى باشلاڭ… + ئىزدەش… + ئۇچۇرلار + بارلىق ئۇقتۇرۇشلارنى ئاۋازسىز قىلىڭ + تاللانغان ھېسابات ھازىر ئىمپورت قىلىندى ۋە ئىشلەتكىلى بولىدۇ + ھەققىدە + ئاكتىپ ئىشلەتكۈچى + ھېسابات قوشۇڭ + ھېسابات ئۆچۈرۈلمەكچى ، ئۇنى ئۆزگەرتىشكە بولمايدۇ + ئاساسلىق تىزىملىكنى ئېچىڭ + قوشۇمچە ھۆججەت قوشۇڭ + Emojis نى قوشۇڭ + سۆھبەتكە قوشۇڭ + قاتناشقۇچىلارنى قوشۇڭ + ياقتۇرىدىغانلارغا قوشۇڭ + ماقۇل ، ھەممىسى تامام! + Pin:%1$s + قۇلۇپ ئېچىش%1$s + كۆك چىش ياڭراتقۇنى قوزغىتىش ئۈچۈن \ \"يېقىن ئەتراپتىكى ئۈسكۈنىلەر \" ئىجازەتنامىسىنى بېرىڭ. + سىنلىق تېلېفون سۈپىتىدە جاۋاب بېرىڭ + پەقەت ئاۋازلىق تېلېفون سۈپىتىدە جاۋاب بېرىڭ + ئاۋاز چىقىرىشنى ئۆزگەرتىش + كامېرانى ئالماشتۇرۇڭ + ئېسىلىڭ + مىكروفوننى ئالماشتۇرۇڭ + رەسىمدىكى ھالەتنى ئېچىڭ + ئۆزى سىنغا ئالماشتۇرۇڭ + INCOMING + سۆھبەت ئىسمى + تېلېفون ئۇقتۇرۇشى + %1$s قولىنى كۆتۈردى + قايتا ئۇلىنىش… + RINGING + تېلېفوندا%1$s + تېلېفون بىلەن%1$s + سىن بىلەن%1$s + 45 سېكۇنتتا جاۋاب يوق ، قايتا سىناڭ + %s تېلېفون + %s سىنلىق سۆزلىشىش + %s ئاۋازلىق تېلېفون + سىن ئالاقىسىنى قوزغىتىش ئۈچۈن \ \"كامېرا \" ئىجازەت بېرىڭ. + بىكار قىلىش + ئىقتىدارغا ئېرىشەلمىدى ، چۈشۈرۈۋېتىلدى + Caption + ھازىرغا قەدەر نامەلۇم SSL گۇۋاھنامىسى%2$sئۈچۈن%1$s تارقىتىلغان ،%3$sدىن%4$s غىچە بولغانلىقىغا ئىشىنەمسىز؟ + گۇۋاھنامىنى تەكشۈرۈپ بېقىڭ + SSL تەڭشىكىڭىز ئۇلىنىشنىڭ ئالدىنى ئالدى + دەلىللەش گۇۋاھنامىسىنى ئۆزگەرتىڭ + پارولنى ئۆزگەرتىش + تەھرىرلەشنى ئەمەلدىن قالدۇرۇڭ + تەھرىرلەشنى ئەمەلدىن قالدۇرۇڭ + بارلىق ئۇچۇرلارنى ئۆچۈرۈڭ + بارلىق ئۇچۇرلار ئۆچۈرۈلدى + بۇ سۆھبەتتىكى بارلىق ئۇچۇرلارنى ئۆچۈرمەكچىمۇ؟ + خېرىدار گۇۋاھنامىسىنى ئۆزگەرتىش + خېرىدار گۇۋاھنامىسى ئورنىتىڭ + ۋە + كۆچۈرۈڭ + چاپلاش تاختىسىغا كۆچۈرۈلدى + قۇر + چەكلەنگەن + خىزمەتتىن ھەيدەش + كەچۈرۈڭ ، چاتاق چىقتى! + تېخىمۇ كۆپ تاللاشلار + Set + ئاتلاش + نامەلۇم + دەلىللەش گۇۋاھنامىسىنى تاللاڭ + ئۇلىنىش… + تامام + سۆھبەت چۈشەندۈرۈشى + سۆھبەت ئۇچۇرى + سىنلىق تېلېفون + ئاۋازلىق تېلېفون + سۆھبەت تېپىلمىدى + سۆھبەت تەڭشەكلىرى + سۆھبەتكە قاتنىشىڭ ياكى يېڭى باشلاڭ + دوستلىرىڭىز ۋە خىزمەتداشلىرىڭىزغا سالام! + كۆچۈرۈڭ + يېڭى سۆھبەت قۇر + راي سىناش + بۈگۈن + تۈنۈگۈن + ئۆچۈرۈش + ھەممىنى ئۆچۈرۈڭ + سۆھبەتنى ئۆچۈرۈڭ + ئەگەر سۆھبەتنى ئۆچۈرسىڭىز ، ئۇ باشقا بارلىق قاتناشقۇچىلار ئۈچۈن ئۆچۈرۈلىدۇ. + ئۇچۇرنى ئۆچۈرۈڭ + ئۇچۇر مۇۋەپپەقىيەتلىك ئۆچۈرۈلدى ، ئەمما ئۇ باشقا مۇلازىمەتلەرگە ئاشكارىلانغان بولۇشى مۇمكىن + ئىشلەتكۈچى%1$s چىقىرىۋېتىلدى + رىياسەتچىدىن تۆۋەنلەش + ئاۋازلىق ئۇچۇرنى خاتىرىلەڭ + ئۇچۇر ئەۋەتىڭ + نۆۋەتتىكى ھېسابات + مۇلازىمېتىر + مۇلازىمېتىر ئۇقتۇرۇش دېتالى ئورنىتىلدى؟ + ئىشلەتكۈچى + ئىشلەتكۈچى ھالىتى قوزغىتىلدى؟ + ئاندروئىد نەشرى + ئەپ + ئەپ ئىسمى + تىزىملاتقان ئابونتلار + ئەپ نۇسخىسى + باتارېيەنى ئەلالاشتۇرۇشقا سەل قارىلىدۇ ، ھەممىسى ياخشى + باتارېيە تەڭشىكى + ئۈسكۈنە + چاتاقنى تەكشۈرۈش تىزىملىكىنى ئېچىڭ + دىئاگنوز قويۇش ئېكرانىنى ئېچىڭ + Dontkillmyapp.com نى ئېچىڭ + ئەڭ يېڭى ئوت ئۆچۈرۈش ئىتتىرىش بەلگىسى + ئەڭ يېڭى ئوت ئۆچۈرۈش بەلبېغى + ئوت ئۆچۈرۈش ئىتتىرىش بەلگىسى يوق. خاتالىق دوكلاتىنى تەييارلاڭ. + Firebase ئىتتىرىش بەلگىسى + Google Play مۇلازىمىتىنى ئىشلەتكىلى بولمايدۇ. ئۇقتۇرۇشنى قوللىمايدۇ + Google Play مۇلازىمىتى + Google Play مۇلازىمىتى بار + ئىتتىرىش ۋاكالەتچىسىدىكى ئەڭ يېڭى ئىتتىرىش + ئىتتىرىش ۋاكالەتچىسىدە تېخى تىزىملىتىلمىدى + مۇلازىمېتىردىكى ئەڭ يېڭى ئىتتىرىش + مۇلازىمېتىردا تېخى تىزىملىتىلمىدى + Meta information + سىستېما دوكلاتىنىڭ ئەۋلادلىرى + چاقىرىش ئۇقتۇرۇشى قانىلى قوزغىتىلدى؟ + ئۇچۇر ئۇقتۇرۇش قانىلى قوزغىتىلدى؟ + ئۇقتۇرۇش ئىجازىتى + تېلېفون + مۇلازىمېتىر پاراڭ نۇسخىسى + مۇلازىمېتىر نەشرى + سىرتقى + ئىچكى + سىگنال ھالىتى + پارول ئىناۋەتسىز + مۇلازىمېتىر ھازىر ئاسراش ھالىتىدە. + ئەپنىڭ ۋاقتى ئۆتتى + بۇ دېتال بەك كونا بولۇپ ، ئەمدى بۇ مۇلازىمېتىر قوللىمايدۇ. يېڭىلاڭ. + يېڭىلاش + بۇ ھېساباتنى قايتا تەستىقلىماقچىمۇ ياكى ئۆچۈرمەكچىمۇ؟ + بۇ مېدىيانى ساقلاشقا ئۈسكۈنىڭىزدىكى باشقا ئەپلەرنى زىيارەت قىلالايدۇ. + داۋاملاشتۇرامسىز؟ + ياق + ساقلاشقا ساقلامسىز؟ + ھەئە + كۆرسىتىش نامىنى تاپقىلى بولمايدۇ ، ئەمەلدىن قالدۇرىدۇ + كۆرسىتىش نامىنى ساقلىيالمىدى ، ئەمەلدىن قالدۇرۇلدى + تەھرىر + تەھرىر + ئۇچۇرنى تەھرىرلەش + باشقۇرغۇچى تەرىپىدىن تەھرىرلەندى + 8 سائەت + 4 ھەپتە + Off + 1 كۈن + 1 سائەت + 1 ھەپتە + پاراڭلىشىش ۋاقتى توشىدۇ + پاراڭلىشىش ئۇچۇرلىرى مەلۇم ۋاقىتتىن كېيىن توشىدۇ. ئەسكەرتىش: پاراڭدا ھەمبەھىرلەنگەن ھۆججەتلەر ئىگىسى ئۈچۈن ئۆچۈرۈلمەيدۇ ، ئەمما سۆھبەتتە ئەمدى ھەمبەھىرلەنمەيدۇ. + سىگنال تەڭشىكىنى ئالالمىدى + قوبۇل قىلىڭ + رەت قىلىش + %1$s دىن%2$s + كۈتۈلمىگەن تەكلىپلەر يوق + تەكلىپنامىڭىز ساقلىنىۋاتىدۇ + قايتىش + ھۆججەتلەرنى زىيارەت قىلىش ئىجازەتنامىسى تەلەپ قىلىنىدۇ + ئاممىۋى ئۇلىنىشقا ئەگىشىدىغان ئىشلەتكۈچى + سىز:%1$s + ئالدىغا + ئالدىغا… + Gallery + مۇلازىمېتىرىڭىز يوقمۇ؟ \ n تەمىنلىگۈچىدىن بىرنى ئېلىش ئۈچۈن بۇ يەرنى چېكىڭ + مەنبە كودىغا ئېرىشىش + گۇرۇپپا + مېھمان + مېھمان زىيارەت قىلىش + مېھمانلارنىڭ زىيارەت قىلىشىنى قوزغىتالمايدۇ ياكى چەكلىيەلمەيدۇ. + مېھمانلارنىڭ ئاممىۋى سۆھبەتتىن ئورتاقلىشىشىغا يول قويۇڭ. + مېھمانلارغا رۇخسەت قىلىڭ + پارول كىرگۈزۈڭ + مېھمان زىيارەت قىلىش پارولى + پارولنى تەڭشەش / چەكلەشتە خاتالىق. + ئاممىۋى ئۇلىنىشنى كىمنىڭ ئىشلىتەلەيدىغانلىقىنى چەكلەش ئۈچۈن پارول بەلگىلەڭ. + پارولنى قوغداش + تەكلىپنامىنى قايتۇرۇڭ + خاتالىق سەۋەبىدىن تەكلىپنامە ئەۋەتىلمىدى. + تەكلىپنامە يەنە ئەۋەتىلدى. + سۆھبەت ئۇلانمىسىنى ھەمبەھىرلەڭ + ئۇچۇر كىرگۈزۈڭ… + باتارېيەنى ئەلالاشتۇرۇشقا سەل قاراشقا بولمايدۇ. ئۇقتۇرۇشنىڭ ئارقا سۇپىدا ئىشلىشىگە كاپالەتلىك قىلىش ئۈچۈن بۇنى ئۆزگەرتىش كېرەك! «جەزملە» نى چېكىپ ، \ \"بارلىق ئەپلەر \" ->%1$s-> ئەلالاشتۇرماڭ + باتارېيەنىڭ ئەلالاشتۇرۇشىغا پەرۋا قىلماڭ + مۇھىم سۆھبەت + تەكلىپنامە + ئوچۇق سۆھبەتكە قاتنىشىڭ + ساقلاڭ + سۆھبەتتىن ئايرىلىشتىن بۇرۇن يېڭى رىياسەتچىنى تەشۋىق قىلىشىڭىز كېرەك + %1$s | ئاخىرقى قېتىم ئۆزگەرتىلگەن:%2$s + سۆھبەتتىن ۋاز كېچىڭ + تېلېفوندىن ئايرىلىش… + GNU ئومۇمىي ئاممىۋى ئىجازەتنامىسى ، 3-نەشرى + ئىجازەتنامە + %s ھەرپ چېكى چەكلەنگەن + Lobby + بۇ يىغىن%1$s غا ئورۇنلاشتۇرۇلغان + يىغىن پات يېقىندا باشلىنىدۇ + سىز ھازىر كۈتۈپخانىدا ساقلاۋاتىسىز. + ھازىرقى ئورنىڭىز + ئورۇن رۇخسىتى تەلەپ قىلىنىدۇ + ئورنى نامەلۇم + قۇلۇپلاندى + قۇلۇپنى چېكىڭ + تەڭشەلمىدى + ئوقۇغاندەك بەلگە قويۇڭ + ئوقۇلمىغان دەپ بەلگە قويۇڭ + مەغلۇب بولدى + ئۇچۇر ئەۋەتمىدى: + تورسىز + جاۋابنى ئەمەلدىن قالدۇرۇڭ + ئۇچۇر ئوقۇلدى + ئۇچۇر ئەۋەتىلدى + ئاۋازلىق ئالاقىنى قوزغىتىش ئۈچۈن \ \"مىكروفون \" ئىجازەت بېرىڭ. + %s دىن كەلگەن تېلېفوننى قولدىن بېرىپ قويدىڭىز + رىياسەتچى + يېڭى سۆھبەت + كۆرۈنۈشچانلىقى + ئوقۇلمىغان تىلغا ئېلىنغان + ئوقۇلمىغان ئۇچۇرلار + %1$s ئىشلەتكىلى بولمايدۇ (باشقۇرغۇچى تەرىپىدىن ئورنىتىلمىغان ياكى چەكلەنمىگەن) + مېھمان + ياق + ئوچۇق سۆھبەت يوق + سىز قاتنىشالايدىغان ئوچۇق سۆھبەت يوق. \ N يا ئوچۇق سۆھبەت يوق ، ياكى ئاللىبۇرۇن ئۇلارنىڭ ھەممىسىگە قوشۇلدىڭىز. + ۋاكالەتچى يوق + ئاۋازنى قوزغىتىشىڭىزغا رۇخسەت قىلىنمايدۇ! + سىننى ئاكتىپلىشىڭىزغا رۇخسەت قىلىنمايدۇ! + ھازىر ئەمەس + %2$s ئۇقتۇرۇش قانىلىدا%1$s + تېلېفون قىلىدۇ + كەلگەن تېلېفونلار ھەققىدە خەۋەر قىلىڭ + ئۇچۇرلار + كەلگەن ئۇچۇرلار ھەققىدە خەۋەر قىلىڭ + يۈكلەش + يوللاش جەريانى ھەققىدە ئۇقتۇرۇش قىلىڭ + ئۇقتۇرۇش تەڭشىكى + ئۇقتۇرۇش تاپشۇرۇۋېلىش ئۈچۈن ئۇقتۇرۇش ئىجازىتى ۋە باتارېيە تەڭشىكى توغرا تەڭشەلدى. قانداقلا بولمىسۇن ئۇقتۇرۇش تاپشۇرۇۋېلىشتا مەسىلىگە يولۇقسىڭىز ، تېلېفون ۋە ئۇچۇرلارنىڭ ئۇقتۇرۇش يوللىرىنىڭ ئوچۇق ياكى ئەمەسلىكىنى تەكشۈرۈڭ. تېخىمۇ كۆپ ياردەملەرنى DontKillMyApp.com ياكى چاتاقلارنى تەكشۈرۈش تىزىملىكىدىن تاپالايسىز. ئەگەر بۇ پايدىسى بولمىسا ، دىئاگنوز قويۇش ئېكرانىغا بېرىپ خاتالىق دوكلاتىنى ئەۋەتىڭ. + ئۇقتۇرۇش كاشىلىسى + ھەمىشە خەۋەر قىلىڭ + تىلغا ئېلىنغاندا ئۇقتۇرۇش قىلىڭ + ھەرگىز ئۇقتۇرماڭ + ھازىر تورسىز ، ئۇلىنىشىڭىزنى تەكشۈرۈپ بېقىڭ + ماقۇل + تىزىملاتقان ئابونتلارغا ئوچۇق سۆھبەت + مېھمان ئەپ ئىشلەتكۈچىلەرگىمۇ ئېچىڭ + ئىگىسى + قاتناشقۇچىلار + قاتناشقۇچىلارنى قوشۇڭ + پارول + ئىجازەت بەلگىلەڭ + بەزى ئىجازەتلەر رەت قىلىندى. + رۇخسەت قىلىڭ + تەڭشەكلەرنى ئېچىڭ + تەڭشەكلەر> ئىجازەتلەر + ھېسابات تېپىلمىدى + %s ئارقىلىق پاراڭ + ئۈنسىز مىكروفون + مىكروفوننى قوزغىتىڭ + ئۇچۇرلار + مەخپىيەتلىك + شەخسىي ئۇچۇر + رىياسەتچىگە تەشۋىق قىلىڭ + ئاممىۋى سۆھبەت + ئىتتىرىش ئۇقتۇرۇشى چەكلەنگەن + پاراڭلىشىش + مىكروفون چەكلەنگەندىن كېيىن «چېكىش» ئارقىلىق «Push-to-talk» نى ئىشلىتىڭ + كېيىن ماڭا ئەسكەرتىڭ + ياقتۇرىدىغانلاردىن ئۆچۈرۈڭ + گۇرۇپپا ۋە ئەزالارنى چىقىرىۋېتىڭ + قاتناشقۇچىنى ئېلىۋېتىڭ + پارولنى ئۆچۈرۈڭ + گۇرۇپپا ۋە ئەزالارنى چىقىرىۋېتىڭ + سۆھبەتنىڭ نامىنى ئۆزگەرتىڭ + ئىسىم ئۆزگەرتىش + جاۋاب + شەخسىي جاۋاب قايتۇرۇڭ + ساقلاش + مۇۋەپپەقىيەتلىك ساقلاندى + 30 سېكۇنت + 5 مىنۇت + 1 مىنۇت + 10 مىنۇت + 600 + 60 + 30 + 300 + ئىزدەش + ئىزدەشنى تازىلاش + ھېسابات تاللاڭ + %1$s s GIF ئەۋەتتى. + سىز بىر سوۋغات ئەۋەتتىڭىز. + %1$s سىن ئەۋەتتى. + سىن ئەۋەتتىڭىز. + %1$s ئاۋاز ئەۋەتتى. + ئاۋاز ئەۋەتتىڭىز. + %1$s رەسىم ئەۋەتتى. + رەسىم ئەۋەتتىڭىز. + %1$s پالۋان كارتىسى ئەۋەتتى + مۇلازىمېتىر ئۇلىنىشىنى سىناش + %1$s سانداننى يېڭىلاڭ + تاللانغان ھېساباتنى ئەكىرىش مەغلۇب بولدى + تور كۆرگۈچتە ئاچقاندا%1$s تور كۆرۈنمە يۈزىگە ئۇلىنىش. + %1$s دېتالىدىن ھېسابات ئەكىرىڭ + ھېسابات ئەكىرىش + %1$s دېتالىدىن ھېسابات ئەكىرىڭ + ھېسابات ئەكىرىش + %1$s نى ئاسراشتىن چىقىرىڭ + %1$s قاچىلاشنى تاماملاڭ + سىناق ئۇلىنىشى + مۇلازىمېتىر قاچىلانغان Talk ئەپنى قوللىمايدۇ + مۇلازىمېتىر ئادرېسى https: //… + %1$s پەقەت%2$s s 13 ۋە ئۇنىڭدىن يۇقىرى ئىشلەيدۇ + يېڭى پارول بەلگىلەڭ + پارول بەلگىلەڭ + تەڭشەك + بار بولغان ھېساباتىڭىز يېڭى ھېسابات قوشۇشنىڭ ئورنىغا يېڭىلاندى + ئىلغار + كۆرۈنۈش + تېلېفون قىلىدۇ + تەڭشەكلەرنى تەكشۈرۈش ياكى خاتالىق دوكلاتىنى تەكشۈرۈش ئۈچۈن دىئاگنوز ئېكرانىنى ئېچىڭ + دىئاگنوز + خاسلاشتۇرۇلغان ئۆگىنىشنى چەكلەش ئۈچۈن كۇنۇپكا تاختىسىغا بۇيرۇق بېرىدۇ (كاپالەتسىز) + Incognito كۇنۇپكا تاختىسى + ئاۋاز يوق + سىز دەلىللىمەكچى بولغان مۇلازىمېتىرغا پاراڭ دېتالى ئورنىتىلمىدى + ئۇقتۇرۇش + ئۇقتۇرۇش رەت قىلىندى + ئۇقتۇرۇش تارقىتىلدى + ئۇچۇرلار + تېلېفون نومۇرىنى ئاساس قىلغان ئالاقىنى ماسلاشتۇرۇپ ، سۆزلىشىش تېزلەتمىسىنى سىستېما ئالاقىلىشىش دېتالىغا بىرلەشتۈرۈڭ + 429 خاتالىق بەك كۆپ + تېلېفون نومۇرىڭىزنى تەڭشىسىڭىز بولىدۇ ، شۇنداق بولغاندا باشقا ئىشلەتكۈچىلەر سىزنى تاپالايدۇ + تېلېفون نومۇرىنى كىرگۈزۈڭ + تېلېفون نومۇرى ئىناۋەتسىز + تېلېفون نومۇرى مۇۋەپپەقىيەتلىك تەڭشەلدى + تېلېفون نومۇرى + تېلېفون نومۇرىنى بىرلەشتۈرۈش + مەخپىيەتلىك + ۋاكالەتچى + ۋاكالەتچى پارول + ۋاكالەتچى ئېغىز + ۋاكالەتچى تىپى + ۋاكالەتچى ئىشلەتكۈچى ئىسمى + مېنىڭ ئوقۇش ھالىتىمنى ھەمبەھىرلەڭ ۋە باشقىلارنىڭ ئوقۇش ھالىتىنى كۆرسىتىڭ + ئوقۇش ھالىتى + ھېساباتقا ئىجازەت بېرىش + ئۆچۈرۈڭ + ھېساباتنى ئۆچۈرۈڭ + نۆۋەتتىكى ھېساباتنى ئۆچۈرۈش نىيىتىڭىزنى جەزملەشتۈرۈڭ. + ئاندىرويىد ئېكران قۇلۇپى ياكى قوللايدىغان بىئولوگىيەلىك ئۇسۇل بىلەن%1$s نى قۇلۇپلاڭ + ئېكران قۇلۇپسىز ھەرىكەت ۋاقتى + ئېكران قۇلۇپى + يېقىنقى تىزىملىك ۋە ئەپ ئىچىدىكى ئېكران كۆرۈنۈشلىرىنىڭ ئالدىنى ئالىدۇ + ئېكران بىخەتەرلىكى + مۇلازىمېتىر نۇسخىسى ناھايىتى كونا بولۇپ ، كېيىنكى نەشرىدە قوللىمايدۇ! + مۇلازىمېتىر نۇسخىسى بەك كونا بولۇپ ، ئاندىرويىد دېتالىنىڭ بۇ نەشرىنى قوللىمايدۇ + قوللىمايدىغان مۇلازىمېتىر + قاراڭغۇ + سىستېما سۈكۈتتىكى ھالىتىنى ئىشلىتىڭ + تېما + نۇر + باشتېما + مېنىڭ خەت بېسىش ھالىتىمنى ھەمبەھىرلەپ ، باشقىلارنىڭ خەت بېسىش ھالىتىنى كۆرسىتىڭ + خەت بېسىش ھالىتى پەقەت يۇقىرى ئىقتىدارلىق ئارقا كۆرۈنۈش (HPB) نى ئىشلەتكەندىلا بولىدۇ + يېزىش ھالىتى + ۋاكالەتچى كىنىشكا تەلەپ قىلىدۇ + ئاگاھلاندۇرۇش + پەقەت نۆۋەتتىكى ھېساباتنىلا رۇخسەت قىلىشقا بولىدۇ + ئالاقىلىشىش + ئالاقىداشلارنى ئوقۇش ئىجازەتنامىسى تەلەپ قىلىنىدۇ + نۆۋەتتىكى ئورۇننى ئورتاقلىشىڭ + ئۇلىنىشنى ھەمبەھىرلەش + ئورۇننى ئورتاقلىشىش + بۇ ئورۇننى ئورتاقلىشىڭ + ھېسابات تاللاڭ + ئورتاق بەھرىلىنىدىغان بۇيۇملار + پالۋان كارتىسى + رەسىم ، ھۆججەت ، ئاۋازلىق ئۇچۇرلار… + ئورتاق بەھرىلىنىدىغان تۈر يوق + ئورنى + ئورتاق ئورۇن + تەرتىپلەش + باشلىنىش ۋاقتى + ھېساباتنى ئالماشتۇرۇش + Team + ھۆججەتلەرنى تاللاڭ + بۇ ھۆججەتلەرنى%1$s غا ئەۋەتىڭ؟ + بۇ ھۆججەتنى %1$s غا ئەۋەتىڭ؟ + كەچۈرۈڭ ، يوللاش مەغلۇپ بولدى + %1$s نى يۈكلىيەلمىدى + مەغلۇبىيەت + %1$s دىن ئورتاقلىشىش + ئۈسكۈنىدىن يۈكلەش + يۈكلەش + %1$s دىن%2$s - %3$s \ %% + رەسىمگە تارتىڭ + سىن ئېلىڭ + ئىشلەتكۈچى + سىن خاتىرىلەش نىسبىتى%1$s + سۆزلىشىش خاتىرىسى(%2$s)%1$s + خاتىرىلەش ، ئەۋەتىش ئۈچۈن قويۇپ بېرىش. + ئاۋاز خاتىرىلەش ئىجازىتى تەلەپ قىلىنىدۇ + «ئەمەلدىن قالدۇرۇش + Webinar + ھەئە + كېلەر ھەپتە + ئىجازەت يوقالغانلىقتىن تېلېفون نومۇرىنى بىرلەشتۈرۈش يوق + بارلىق ئۇچۇرلار + \@ -mentions only + Off + كۆڭۈلدىكى + 1 سائەت + توردا + توردىكى ئورنى + ئوچۇق سۆھبەت + ھۆججەتلەر دېتالىدا ئېچىڭ + ئاۋازلىق ئۇچۇرنى قويۇش / توختىتىش + تاللاش قوشۇڭ + بېلەتنى تەھرىرلەش + راي سىناشنى ئاخىرلاشتۇرۇڭ + بۇ راي سىناشنى راستىنلا ئاخىرلاشتۇرماقچىمۇ؟ بۇنى ئەمەلدىن قالدۇرغىلى بولمايدۇ. + بۇ راي سىناشقا تېخىمۇ كۆپ تاللاشلار بىلەن بېلەت تاشلىيالمايسىز. + كۆپ جاۋاب + تاللانما + شەخسىي راي سىناش + سوئال + سوئالىڭىز + نەتىجە + تەڭشەك + بېلەت تاشلاش + بېلەت تاشلاندى + ئىلگىرى تەڭشەلگەن + قولىنى كۆتۈرۈڭ + ھەممىسى + ھۆججەتلەرنى ساقلاشتىن ئورتاقلىشىش رۇخسەتسىز مۇمكىن ئەمەس + تېلېفون خاتىرىلىنىۋاتىدۇ + خاتىرىلەشنى باشلاشنى ئەمەلدىن قالدۇرۇڭ + خاتىرىلەش مەغلۇپ بولدى. باشقۇرغۇچىڭىز بىلەن ئالاقىلىشىڭ. + خاتىرىلەشنى باشلاڭ + خاتىرىلەشنى توختاتماقچىمۇ؟ + چاقىرىش خاتىرىسىنى توختىتىڭ + خاتىرىلەشنى توختىتىڭ + خاتىرىلەشنى توختىتىش… + بارلىق تېلېفونلاردا خاتىرىلەش ئىجازىتى تەلەپ قىلىنىدۇ + بۇ خاتىرىدە ئاۋازىڭىز ، كامېرادىكى سىن ۋە ئېكران ھەمبەھىرلىنىشىڭىز بولۇشى مۇمكىن. تېلېفونغا قاتنىشىشتىن بۇرۇن سىزنىڭ رۇخسىتىڭىز تەلەپ قىلىنىدۇ. ماقۇلمۇ؟ + بۇ سۆھبەتكە چاقىرىشتىن بۇرۇن خاتىرىلەش ئىجازىتىنى تەلەپ قىلىڭ + خاتىرىلەش + تېلېفون خاتىرىلەنگەن بولۇشى مۇمكىن. + خاتىرىلەش + ياقتۇرىدىغان سۆزلەردىن%1$s ئۆچۈرۈلدى + سۆھبەت%1$s غا ئۆزگەرتىلدى + ھالىتىنى ئەسلىگە كەلتۈرۈش + تېلېفون قىلغاندا باشقا ياتاقلارغا قوشۇلۇش مۇمكىن ئەمەس + ساقلاش + Scan QR Code + پەقەت ئىشەنچلىك مۇلازىمېتىرلارغا ماسقەدەملەڭ + فېدېراتسىيە + پەقەت بۇ مىسالدىكى كىشىلەر ۋە مېھمانلارلا كۆرۈنىدۇ + يەرلىك + پەقەت كۆچمە تېلېفوندا سۆزلىشىش ئارقىلىق تېلېفون نومۇرىنى بىرلەشتۈرۈش ئارقىلىق ماسلاشقان كىشىلەرگە كۆرۈندى + شەخسىي + ئىشەنچلىك مۇلازىمېتىرلار ۋە دۇنياۋى ۋە ئاممىۋى ئادرېس دەپتىرىگە ماسقەدەملەڭ + ئېلان قىلىندى + دائىرە ئالماشتۇرۇش + %1$s نىڭ مەخپىيەتلىك دەرىجىسىنى ئۆزگەرتىڭ + ئاستىغا يۆتكەڭ + سىنبەلگە ئىزدەش + سېكۇنت بۇرۇن + تاللانغان + ئېلېكترونلۇق خەت ئەۋەتىڭ + ئەۋەتىش + بۇ سۆھبەتكە مەزمۇن ئورتاقلىشىڭىزغا رۇخسەت قىلىنمايدۇ + ئەۋەتىش… + ئۇقتۇرۇشسىز ئەۋەتىڭ + Set + كامېرادىن باش سۈرىتىنى تەڭشەڭ + ھالەت بەلگىلەڭ + ھالەت ئۇچۇرىنى بەلگىلەڭ + ھەمبەھىرلەش + %2$s دىكى سۆھبەتكە قوشۇلۇڭ%1$s + Audio + ھۆججەت + Media + باشقىلىرى + راي سىناش + چاقىرىش خاتىرىسى + ئاۋاز + چەكلەش سەۋەبىنى كۆرسىتىڭ + چەكلەنگەن قاتناشقۇچىلارنى كۆرسىتىش + ئامراق + سىزنىڭ تېلېفوننى باشلىشىڭىزغا رۇخسەت قىلىنمايدۇ + تېلېفون باشلىدى + ھالەت ئۇچۇرى + بۆسۈش ئۆيىگە ئالماشتۇرۇڭ + ئاساسلىق ئۆيگە ئالماشتۇرۇڭ + رەسىمگە تارتىڭ + رەسىمگە تارتىشتا خاتالىق + رۇخسەتسىز رەسىمگە تارتىش مۇمكىن ئەمەس + قايتا سۈرەتكە تارتىڭ + ئەۋەتىڭ + كامېرا ئالماشتۇرۇش + Crop photo + رەسىمنىڭ چوڭ-كىچىكلىكىنى ئازايتىش + مەشئەلنى ئالماشتۇرۇڭ + 30 مىنۇت + بۇ ھەپتە + بۇ بىر سىناق ئۇچۇرى + بۇ ھەپتە ئاخىرى + جاۋاب + بۈگۈن + ئەتە + تەرجىمە + تەرجىمە + تەرجىمە قىلىنغان تېكىستنى كۆچۈرۈڭ + تىلنى ئېنىقلاش + ئۈسكۈنىنىڭ تەڭشىكى + تىلنى بايقىيالمىدى + تەرجىمە مەغلۇب بولدى + From + To + يەنە 1 بولسا خەت بېسىۋاتىدۇ… + are typing… + يېزىۋاتىدۇ… + ۋە%1$s باشقىلار يېزىۋاتىدۇ… + قالايمىقان سۆھبەت + سۆھبەت بىر تەرەپ قىلىنمىغاندىن كېيىن ، سۈكۈتتىكى ھالەتتە يەنە كۆرسىتىلىدۇ. + Unban + ئوقۇمىغان + ئۈسكۈنىدىن يېڭى باش سۈرىتىنى يۈكلەڭ + ئالماشتۇرۇش: + ئىشلەتكۈچى باش سۈرىتى + ئادرېس + تولۇق ئىسمى + ئېلخەت + تېلېفون نومۇرى + Twitter + تور بېكەت + ھالەت + شەخسىي ئىشلەتكۈچى ئۇچۇرىغا ئېرىشەلمىدى. + شەخسىي ئۇچۇر يوق + ئارخىپ بېتىڭىزگە ئىسىم ، رەسىم ۋە ئالاقىلىشىش تەپسىلاتلىرىنى قوشۇڭ. + سىنلىق تېلېفون + ئەھۋالىڭىز نېمە؟ + + %d vote + %d votes + + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000..cab7c82 --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,719 @@ + + + Редагувати + Додати + Додати до нотаток + Додано розмову %1$s до обраного + Пошук у %s + Перебуваю поза мережею + Архівна розмова + Після архівування розмова буде прихована за замовчуванням. Виберіть фільтр «Архів» для перегляду архівних розмов. Прямі згадки все одно будуть надходити. + Заархівовані + Архівовані %1$s + Аудіовиклик + Bluetooth + Вивід аудіо + Телефон + Колонки + Дротова гарнітура + Ваш статус встановлено автоматично + Світлина + Немає поряд + Кнопка \"Назад + Заборона + Забанити учасника + Список заборон + Зайнято + Календар + Розширені опції дзвінка + Дзвінок триває вже годину. + Дзвінок без сповіщення + Дозвіл на використання камери надано. Будь ласка, виберіть камеру ще раз. + Скасувати авторизацію + Обрати світлину в хмарі + Очистити повідомлення про стан + Очистити повідомлення про стан після + Закрити + Закрити іконку + З\'єднання встановлено + Немає з\'єднання з сервером + Втрачено з\'єднання - Надіслані повідомлення стоять у черзі + Блокування запису для безперервного запису голосового повідомлення + Розмова заархівована + Розмова доступна тільки для читання + Не вдалося встановити розмову Тільки для читання + Розмови + Створити розмову + Створити проблему + Власне + Небезпечна зона + %1$s в %2$s + Вилучити світлину + Видалити запис голосу + Видалено розмову %1$s + Не турбувати + Не очищати + Редагувати + Повідомлення старше 24 годин не можуть бути відредаговані + Редагувати повідомлення + Нещодавні + Зашифровано + Кінець зв\'язку. + Завершити дзвінок для всіх + Виникла проблема під час завантаження ваших чатів + Виникла помилка при знятті бану з учасника + Не вдалося зберегти %1$s + 15 хвилин + каталог + Завантаження … + %1$s (%2$d) + Відстежувані теми + 4 години + Не вдалося отримати очікувані запрошення + (відредаговано) + Внутрішня примітка + Невидимка + Мови не вдалося отримати + Не вдалося виконати пошук + Пізніше сьогодні + Вийти + Ви покинули розмову %1$s + Завантажити більше результатів + Місцевий час: %1$s + Заблокувати розмову + Символ блокування + Опустити руку + Позначена розмова %1$s як прочитана + Позначив розмову %1$s як непрочитану + Згадані + Новіші спочатку + Старіші спочатку + А--Я + Я--А + Найбільші за розміром спочатку + Найменші за розміром спочатку + Повідомлення скопійовано + Ви вилучили повідомлення + За редакцією %1$s + Натисніть, щоб відкрити опитування + Нічого не знайдено + Що шукаємо? + Пошук … + Повідомлення + Вимкнути всі сповіщення + Обраний обліковий запис імпортовано і він доступний + Про застосунок + Активний користувач + Додати обліковий запис + Заплановано вилучення облікового запису, тому до нього неможливо внести зміни + Відкрити головне меню + Додати вкладення + Додати емоційки + Додати до розмови + Додати учасників + Додати зірочку + ОК, все готово! + ПІН: %1$s + Розблокувати %1$s + Щоб увімкнути Bluetooth-динаміки, надайте дозвіл \"Пристрої поблизу\". + Відповісти як відеодзвінок + Відповісти тільки як голосовий виклик + Змінити аудіовихід + Перемикач камери + Покласти слухавку + Перемикач мікрофона + Відкрийте режим \"картинка в картинці + Переключіться на відео з собою + ВХІДНИЙ + Назва розмови + Сповіщення про дзвінки + %1$s підняв руку + Відновлення зєднання... + ВИКЛИК + %1$s у виклику + %1$s з телефоном + %1$sз відео + Немає відповіді 45 секунд, торкніться щоб повторити + %s телефонує + %s, відеодзвінок + %s, голосовий дзвінок + Щоб увімкнути відеозв\'язок, будь ласка, надайте дозвіл \"Камера\". + Скасувати + Не вдалося розпізнати можливості, скасування + Підпис + Чи довіряєте ви невідомому сертифікату, виданому %1$s для %2$s, що дійсний з %3$s до %4$s? + Перевірка сертифікату + Ваші налаштування SSL завадили з’єднанню + Змінити сертифікат авторизації + Змінити Пароль + Скасувати редагування + Скасувати редагування + Вилучити усі повідомлення + Всі повідомлення видалено + Ви дійсно бажаєте вилучити усі повідомлення у цій розмові? + Змінити сертифікат клієнта + Встановити сертифікат клієнта + та + Копіювати + Скопійовано + Створити + Вимкнено + Припинити + Вибачте, щось пішло не так! + Більше варіантів + Встановити + Пропустити + Невідомо + Вибрати сертифікат авторизації + Підключення ... + Готово + Опис розмови + Інформація про розмову + Відеодзвінок + Голосовий дзвінок + Розмову не знайдено + Налаштування розмови + Приєднайтеся до розмови або почніть нову + Привітайтеся з вашими друзями та колегами! + Копіювати + Створити нову розмову + Створити опитування + Ти: + Сьогодні + Вчора + Вилучити + Вилучити все + Вилучити розмову + Якщо ви вилучите розмову, її буде вилучено у всіх інших учасників цієї розмови. + Вилучити повідомлення + Повідомлення вилучено, але воно могло потрапити до інших сервісів + Видалити зараз + Користувача %1$s було видалено + Розжалування з модератора + Записати голосове повідомлення + Надіслати повідомлення + Поточний обліковий запис + Сервер + Чи встановлено додаток для сповіщення про сервер? + Користувач + Статус користувача увімкнено? + Версія для Android + Застосунок + Зазначте назву застосунку + Зареєстровані користувачі + Версія програми + Оптимізація батареї ігнорується, все добре + Оптимізація батареї увімкнена, що може спричинити проблеми. Вам слід вимкнути оптимізацію акумулятора! + Налаштування батареї + Пристрій + Відкрийте контрольний список усунення несправностей + Відкрийте екран діагностики + Відкрийте dontkillmyapp.com + Остання вибірка токенів push-бази вогню + Останнє покоління пуш-токенів firebase + Немає встановленого пуш-токена бази даних. Будь ласка, створіть звіт про помилку. + Push-токен Firebase + Сервіси Google Play недоступні. Сповіщення не підтримуються + Сервіси Google Play + Доступні сервіси Google Play + Остання реєстрація пушу на пуш-проксі + Ви ще не зареєстровані на push-проксі + Остання реєстрація push на сервері + Ще не зареєстрований на сервері + Метаінформація + Формування системного звіту + Канал сповіщення про дзвінки увімкнено? + Канал сповіщення про повідомлення увімкнено? + Дозволи на сповіщення + Телефон + Версія Server Talk + Версія сервера + Зовнішнє + Внутрішній + Режим сигналізації + Invalid Password + Наразі сервер знаходиться в режимі технічного обслуговування. + Додаток застарілий + Додаток занадто старий і більше не підтримується цим сервером. Будь ласка, оновіть його. + Оновлення + Чи хочете ви повторно авторизувати чи вилучити цей акаунт? + Збереження цього медіафайлу в пам\'яті дозволить будь-яким іншим програмам на вашому пристрої отримати до нього доступ. + Продовжувати? + Ні + Зберегти в сховище? + Так + Ім\'я для показу не вдалося розпізнати. Скасування + Неможливо отримати ім\'я для показу. Скасування + Редагувати + Редагувати + Редагувати повідомлення + Відредаговано адміністратором + Меню розмови про подію + Розклад + 8 годин + 4 тижні + Вимкнути + 1 день + 1 година + 1 тиждень + Термін дії повідомлень чату закінчився + Повідомлення чату можуть бути видалені після закінчення певного часу. Примітка: Файли, передані в чаті, не будуть видалені для власника, але більше не будуть доступні в обговоренні. + Помилка отримання налаштувань сповіщення + Прийняти + Відхилити + з %1$s на %2$s + Немає очікуваних запрошень + У вас є очікувані запрошення + Назад + Потрібен дозвіл на доступ до файлів + Фільтрувати розмови + Користувач, який увійшов за посиланням + Ви: %1$s + Переслати + Переслати … + Галерея + Ще не маєте сервера?\nНатисніть тут щоб отримати в провайдера + Отримати вихідний код + Група + Гість + Гостьовий доступ + Неможливо ввімкнути/вимкнути гостьовий доступ. + Дозвольте гостям ділитися публічним посиланням, щоб приєднатися до цієї розмови. + Дозволити гостей + Enter a password + Пароль гостьового доступу + Помилка під час встановлення/відключення пароля. + Встановіть пароль, щоб обмежити доступ до публічного посилання. + Захист паролем + Повторно надіслати запрошення + Запрошення не було надіслано через помилку, що виникла. + Запрошення було розіслано повторно. + Поділитися посиланням на бесіду + Введіть повідомлення ... + Оптимізація батареї не ігнорується. Це слід змінити, щоб повідомлення працювали у фоновому режимі! Натисніть «OK» і виберіть «Усі програми» -> %1$s -> Не оптимізувати. + Ігнорувати оптимізацію акумулятора + Важлива розмова + Статус користувача \"Не турбувати\" ігнорується для важливих розмов + Неправильний час + Запрошення + Приєднуйтесь до відкритих розмов + Зберегти + Перед тим, як вийти з розмови, вам потрібно призначити нового модератора + %1$s | Остання зміна: %2$s + Вийти з групи + Завершення виклику ... + GNU General Public License, Version 3 + Ліцензія + %sсимволів лишилося + Вітальня + Ця зустріч запланована на %1$s + Зустріч незабаром розпочнеться + Зараз ви чекаєте у Вітальні + Ваше місце розташування + Обов\'язково потрібен доступ до місця розташування + Місце розташування не визначено + Заблоковано + Торкніться щоб розблокувати + Не встановлено + Позначити прочитаним + Позначити не прочитаним + Розмова, позначена як важлива + Розмова, не позначена як конфіденційна + Розмова позначена як конфіденційна + Розмова, не позначена як важлива + Зустріч закінчилася + Повідомлення додано до нотаток + Не вдалося + Не вдалося надіслати повідомлення: + Офлайн + Скасувати відповідь + Повідомлення прочитано + Відправлення + Повідомлення відправлено + Мікрофон увімкнено, йде запис звуку + Щоб увімкнути голосовий зв\'язок, будь ласка, надайте дозвіл \"Мікрофон\". + Ви пропустили дзвінок з номера %s + Модератор + Нова розмова + Видимість + Непрочитані згадки + Непрочитані повідомлення + Застосунок %1$s недоступний (не встановлений, або використання додатка обмежене адміністратором) + Гість + Ні + Ніяких відкритих розмов + Немає відкритих розмов, до яких ви можете приєднатися.\nАбо немає відкритих розмов, або ви вже приєдналися до всіх них. + Без проксі + У вас відсутній доступ на увімкнення аудіо! + У вас відсутній доступ на увімкнення відео! + Не зараз + канал повідомлень %1$s на %2$s + Виклики + Сповіщати про вхідні дзвінки + Повідомлення + Сповіщати про вхідні повідомлення + Завантаження + Сповіщення про хід завантаження + Налаштування сповіщень + Сповіщення налаштовані неправильно + Дозвіл на отримання сповіщень та налаштування акумулятора правильно налаштовані для отримання сповіщень. Якщо у вас все одно виникають проблеми з отриманням сповіщень, перевірте, чи ввімкнені канали сповіщень для дзвінків та повідомлень. Додаткову допомогу можна знайти на сайті DontKillMyApp.com або в переліку заходів з усунення несправностей. Якщо це не допоможе, перейдіть до екрана діагностики та надішліть звіт про помилку. + Усунення несправностей у сповіщеннях + Завжди сповіщувати + Сповіщати на зустрічах + Ніколи не сповіщати + Зараз оффлайн, перевірте з\'єднання + OK + Зустріч, що триває + Відкрита бесіда для зареєстрованих користувачів + Також відкрито для користувачів гостьових додатків + Власник + Учасники + Додати учасників + Пароль + Встановіть дозволи + У деяких дозволах було відмовлено. + Будь ласка, надайте дозволи + Відкрити налаштування + Будь ласка, надайте дозволи в Налаштуваннях > Дозволи + Акаунт не знайдено + Чат з %s + Вимкнути мікрофон + Увімкнути мікрофон + Повідомлення + Конфіденційність + Персональна інформація + Призначити модератором + Публічна розмова + Push сповіщення вимкнено + Вибачте, щось пішло не так, помилка %1$s + Вибачте, щось пішло не так, не вдається отримати тестове push-повідомлення + Push-повідомлення успішно надіслано. Ви повинні отримати на цьому пристрої повідомлення з назвою «Тестування push-повідомлень». + Тисни-та-кажи + Натисніть і утримуйте & для використання PTT при вимкненому мікрофоні + Нагадати пізніше + Прибрати зірочку + Вилучити групи та учасників + Вилучити учасника + Видалити пароль + Видалити команду та учасників + Перейменувати розмову + Перейменувати + Відповісти + Відповісти у приватній розмові + Приміщення успішно утримується + Зберегти + Збережено успішно + 30 секунд + 5 хвилин + Одна хвилина + 10 хвилин + Негайно + 600 + 60 + 30 + 300 + Пошук + Очистити пошук + Оберіть обліковий запис + Повідомлення про оновлення + Надіслати запис голосу + Делікатна розмова + Попередній перегляд повідомлень буде вимкнено у списку розмов та сповіщеннях + %1$s надсилає GIF. + Ви надіслали GIF. + %1$s надсилає відео. + Ви надіслали відео. + %1$s надсилає аудіо. + Ви надіслали аудіо. + %1$s надсилає зображення. + Ви надіслали зображення. + %1$s надіслав карту колоди + Перевірка з\'єднання з сервером + Будь ласка, оновіть вашу %1$s базу даних + Не вдалося імпортувати обраний акаунт. + Посилання на ваш веб-інтерфейс %1$s, коли ви відкриєте його у веб-переглядачі. + Імпортувати обліковий запис із застосунку %1$s + Імпорт акаунту + Імпортувати облікові записти із застосунку %1$s + Імпорт акаунтів + Будь ласка, вимкніть режим обслуговування для %1$s + Будь ласка, завершіть встановлення %1$s + Перевірка з\'єднання + На сервері не встановлено застосунок \"Talk\", який може підтримуватися + Адреса сервера https://… + Застосунок %1$s сумісний з %2$s версії 13 або вище + Встановіть новий пароль + Встановити пароль + Налаштування + Замість створення нового облікового запису було виконано оновлення існуючого + Додатково + Вигляд + Виклики + Будь ласка, зв\'яжіться з адміністратором + Відкрийте екран діагностики, щоб перевірити налаштування або створити звіт про помилку + Діагноз + Вказує клавіатурі відключити персоналізоване навчання (без гарантій) + Клавіатура інкогніто + Відсутній звук + На сервері, до якого ви намагаєтеся підключитися, застосунок \"Talk\" не встановлено. + Сповіщення + Сповіщення відхиляються + Сповіщення надаються + Повідомлення + Контакти, поруч з якими з\'явиться значок застосунку \"Talk\" у застосунку системних контактів + Помилка 429 Забагато запитів + Ви можете встановити свій номер телефону, щоб інші користувачі могли вас знайти + Зазначте номер телефону + Неправильний номер телефону + Номер телефону успішно встановлено + Номер телефону + Інтеграція номера телефону + Конфіденційність + Вузол проксі + Пароль проксі + Порт проксі + Тип проксі + Ім\'я користувача проксі + Ділитися своїм статусом читання та показувати статус читання інших + Звіт про прочитання + Повторно авторизувати обліковий запис + Вилучити + Вилучити обліковий запис + Будь ласка, підтвердіть вилучення поточного облікового запису. + Захищати %1$s за допомогою системи блокування Android або підтримуваного біометричного методу + Затримка блокування екрана під час бездіяльності + Блокування екрана + Запобігає збереженню скріншотів усередині додатка і в списку недавніх додатків + Безпека екрану + Ця версія серверу дуже застаріла і не буде підтримуватися в наступному випуску! + Версія сервера застаріла і не підтримується цією версією застосунку для Android + Непідтримуваний сервер + Не встановлено програму сповіщень сервера + Встановлюється за допомогою економії заряду батареї + Темна + Використовуйте системний за замовчуванням + тема + Світла + Тема + Діліться своїм статусом набору тексту та показуйте статус набору тексту інших + Статус набору тексту доступний лише при використанні високопродуктивного бекенда (HPB) + Стан набору тексту + Для проксі потрібні облікові дані + Увага + Повторний вхід може бути виконаний тільки в поточний обліковий запис + Поділіться контактом + Потрібен дозвіл на читання контактів + Поділіться поточним місцем розташування + Доступ за посиланням + Поділіться місцем розташування + Поділіться цим розташуванням + Оберіть обліковий запис + Спільні елементи + Картка + Зображення, файли, голосові повідомлення ... + Немає спільних елементів + Місце + Місце у спільному доступі + Якщо сповіщення налаштовані неправильно, показувати звичайне попередження + Показувати регулярні сповіщення про попередження + Впорядкувати за + Почніть груповий чат + Час початку + Перемкнути обліковий запис + Команда + Протестуйте пуш-сповіщення + Результати тестування + Сьогодні на %1$s + Завтра в %1$s + Виберіть файли + Надіслати ці файли на %1$s? + Надіслати цей файл на %1$s? + Вибачте, помилка завантаження + Не вдалося завантажити %1$s + Невдача + Поділіться з %1$s + Завантажити з пристрою + Uploading + %1$s - %2$s - %3$s\%%. + Сфотографувати + Зняти відео + Користувач + Запис відео з %1$s + Запис розмови з %1$s (%2$s) + Утримуйте для запису, відпустіть для надсилання. + Потрібен дозвіл на аудіозапис + « Зсунути для скасування + Вебінар + Так + Наступний тиждень + Розмови не архівуються + Не збережено жодного офлайн-повідомлення + Відсутні дозволи на інтеграцію номера телефону + Вимкнено + Типово + Слідкувати за налаштуваннями розмови + 1 година + Онлайн + Статус онлайну + Відкрити розмови + Відкрити у застосунку \"Файли\" + Відкриті нотатки + Перейти до теми + Відтворення/пауза голосового повідомлення + Регулювання швидкості відтворення + Додати варіант + Змінити голос + Завершити опитування + Ви справді хочете закінчити це опитування? Це не можна скасувати. + Ви не можете голосувати більше ніж за один варіант у цьому опитуванні. + Кілька відповідей + Видалити параметр %1$d + Варіант %1$d + Параметри + Приватне опитування + Запитання + Ваше питання + Результат + Налаштування + Голосувати + Голос прийнято + Раніше встановлений + QR-код не вдалося зчитати + Підняти руку + Всі + Загальний доступ до файлів зі сховища неможливий без дозволів + Останні теми + Дзвінок записується + Скасувати початок запису + Запис не відбувся. Зверніться до адміністратора. + Почати запис + Ви дійсно хочете зупинити запис? + Зупинити запис дзвінка + Зупинити запис + Зупинка запису ... + Згода на запис необхідна для всіх дзвінків + Запис може містити ваш голос, відео з камери та спільний доступ до екрана. Перед приєднанням до дзвінка необхідна ваша згода. Чи даєте ви згоду? + Вимагати згоди на запис перед тим, як приєднатися до розмови в цій розмові + Згода на запис + Дзвінок може бути записаний. + Запис + Видалив розмову %1$s з обраного + Розмову %1$s було перейменовано + Повторно відправити + Скинути статус + Неможливо приєднатися до інших номерів під час розмови + Зберегти + Scan QR Code + Синхронізовувати лише з серверами, яким довіряємо + Federated + Видно лише користувачам цієї хмари та гостям + Локально + Відображається лише людям, зіставленим за допомогою інтеграції номера телефону через Talk на мобільному пристрої + Приватно + Синхронізація з надійними серверами та глобальною та публічною адресною книгою + Опубліковано + Перемикання діапазону + Змінити рівень конфіденційності %1$s + Прокрутіть донизу + Значок пошуку + секунд тому + Selected + Надіслати + Надсилайте на адресу + Ви не маєте права ділитися контентом у цьому чаті + Надіслати на ... + Надіслати без попередження + Встановити + Встановити аватарку з камери + Встановити статус + Оновити статус + Спільний доступ + Приєднуйтесь до розмови %1$s на %2$s + Аудіо + Файл + Зображення та відео + Інші + Опитування + Запис розмови + Голос + Показати причину бану + Показати заборонених учасників + Із зірочкою + Ви не маєте права починати розмову + Створіть тему + почала дзвонити. + Повідомлення про статус + Статус скасовано + Перейдіть до кімнати для переговорів + Перейдіть до головної кімнати + Сфотографуйте + Помилка під час зйомки + Фотографування неможливе без дозволу + Перефотографуйте + Надіслати + Переключити камеру. + Кадрувати зображення + Зменшити розмір зображення + Тумблерний ліхтар + 30 хвилин + Цього тижня + Це тестове повідомлення + Цими вихідними + Відхилити створення гілки + Назва гілки + Сьогодні + Завтра + Перекласти + Переклад + Скопіювати перекладений текст + Визначити мову + Налаштування пристрою + Не вдалося визначити мову + Не вдалося перекласти + Від + Кому + і ще 1 набирає текст ... + друкують ... + друкує ... + і %1$s інші набирають ... + Неархівована розмова + Після того, як розмова буде розархівована, вона знову буде показана за замовчуванням. + Неархівовані %1$s + Розбан + Непрочитано + Завантажити нову аватарку з пристрою + %1$s не на посаді і може не відповісти + %1$s сьогодні не на посаді + Заміна: + Аватар користувача + Адреса + Повна назва + Електронна пошта + Номер телефону + Twitter + Вебсайт + Статус + Не вдалося отримати особисту інформацію користувача. + Відсутня особиста інформація + Додайте назву, зображення та деталі контакту на сторінці вашого профілю. + Відеодзвінок + Який твій статус? + + Див. схоже повідомлення %d + Дивіться %d схожих повідомлень + Дивіться %d схожих повідомлень + Дивіться %d схожих повідомлень + + + Ця розмова буде автоматично видалена для всіх через %1$d день без активності. + Ця розмова буде автоматично видалена для всіх через %1$d дня без активності. + Ця розмова буде автоматично видалена для всіх через %1$d днів без активності. + Ця розмова буде автоматично видалена для всіх через %1$d днів без активності. + + + %d голосів + %d голосів + %d голосів + %d голосів + + diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml new file mode 100644 index 0000000..47933e7 --- /dev/null +++ b/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/app/src/main/res/values-v28/arrays.xml b/app/src/main/res/values-v28/arrays.xml new file mode 100644 index 0000000..baf10f3 --- /dev/null +++ b/app/src/main/res/values-v28/arrays.xml @@ -0,0 +1,64 @@ + + + + + @string/nc_no_proxy + HTTP + DIRECT + SOCKS + + + + @string/nc_notify_me_never + @string/nc_notify_me_mention + @string/nc_notify_me_always + + + + never + mention + always + + + + @string/nc_screen_lock_timeout_immediate + @string/nc_screen_lock_timeout_30 + @string/nc_screen_lock_timeout_60 + @string/nc_screen_lock_timeout_300 + @string/nc_screen_lock_timeout_600 + + + + @string/nc_screen_lock_timeout_immediate + @string/nc_screen_lock_timeout_thirty + @string/nc_screen_lock_timeout_sixty + @string/nc_screen_lock_timeout_three_hundred + @string/nc_screen_lock_timeout_six_hundred + + + + 1 + 30 + 60 + 300 + 600 + + + + @string/nc_settings_theme_follow_system + @string/nc_settings_theme_light + @string/nc_settings_theme_dark + + + + @string/nc_settings_theme_follow_system_key + @string/nc_settings_theme_light_key + night_yes + + + diff --git a/app/src/main/res/values-v28/defaults.xml b/app/src/main/res/values-v28/defaults.xml new file mode 100644 index 0000000..5243cce --- /dev/null +++ b/app/src/main/res/values-v28/defaults.xml @@ -0,0 +1,10 @@ + + + + @string/nc_settings_theme_follow_system_key + \ No newline at end of file diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..852f748 --- /dev/null +++ b/app/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,13 @@ + + + + + 64dp + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000..2c527b2 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,586 @@ + + + 编辑 + 添加 + 对话%1$s已添加至收藏 + 搜索位置 %s + 显示为离线 + 存档对话 + 已存档 + 蓝牙 + 音频输出 + 电话 + 扬声器 + 有线耳机 + 您的状态已自动设定 + 头像 + 离开 + 忙碌 + 日历 + 高级呼叫选项 + 通话已持续一小时 + 无通知呼叫 + 摄像头访问已授权。请重新选择摄像头。 + 取消登录 + 从云端选择头像 + 清除状态消息 + 过多长时间清除状态消息 + 关闭 + 连接已建立 + 锁定录音以连续录制语音信息 + 对话 + 建立对话 + 自定义 + 危险区域 + 删除头像 + 对话%1$s已删除 + 不要打扰 + 不要清除 + 编辑 + 编辑消息 + 最近 + 已加密 + 结束通话 + 为所有人结束通话 + 加载聊天记录时出错 + 保存%1$s失败 + 15 分钟 + 文件夹 + 正在加载 … + %1$s (%2$d) + 关注的帖子 + 4小时 + (已编辑) + 隐身 + 今日稍晚 + 离开通话 + 你已离开对话%1$s + 加载更多结果 + 锁定对话 + 锁定头像 + 放下手 + 已将对话%1$s标记为已读 + 已将对话%1$s标记为未读 + 已提及 + 最新的在前 + 最旧的在前 + A - Z + Z - A + 大文件在前 + 小文件在前 + 消息被你删除了 + 由 %1$s 编辑 + 点击打开投票 + 无搜索结果 + 开始输入以搜索… + 搜索... + 消息 + 静音所有通知 + 所选帐户现已导入并可用 + 关于 + 活跃用户 + 添加账号 + 此账号已计划删除,且无法更改 + 打开主菜单 + 添加附件 + 添加 emoji + 添加到对话 + 添加参与者 + 添加收藏 + OK,已完成! + 固定:%1$s + 解锁 %1$s + 使用蓝牙音箱需要“附近的设备”权限 + 以视频通话接听 + 以语音通话接听 + 切换音频输出 + 开启/关闭摄像头 + 挂断 + 开启/关闭麦克风 + 开启画中画模式 + 切换到自拍画面 + 来电 + 对话名称 + 呼叫通知 + %1$s 举手了 + 正在重新连接 ... + 响铃 + %1$s 在通话中 + %1$s 通过手机 + %1$s 在进行视频通话 + 45秒内无响应,点击以重试 + %s 通话 + %s 语音通话 + %s 语音通话 + 使用视频通话需要“相机”权限 + 取消 + 获取容量已失败,正在中止 + 您是否信任由 %1$s 颁发给 %2$s,生效时间从 %3$s 至 %4$s的未知证书? + 检查证书 + 您的 SSL 设置阻止了连接 + 更换验证证书 + 更改密码 + 取消编辑 + 取消编辑 + 删除所有消息 + 已删除所有消息 + 你真想删除对话中的所有信息吗? + 修改客户端证书 + 设置客户端证书 + + 复制 + 已复制到剪贴板 + 创建 + 已禁用 + 不予理会 + 抱歉,出问题了! + 更多选项 + 设置 + 跳过 + 未知 + 选择验证证书 + 连接中…… + 完成 + 对话描述 + 对话信息 + 视频通话 + 语音通话 + 没有找到对话 + 对话设置 + 加入一个对话或开始一个新的对话 + 跟你的朋友和同事打个招呼! + 复制 + 创建一个新对话 + 创建投票 + 您: + 今天 + 昨天 + 删除 + 删除全部 + 删除对话 + 如果你删除了该对话,它也会被从其他所有与会者那里删除。 + 删除邮件 + 消息已成功删除,但可能已泄漏给其他服务 + 立即删除 + 取消主持人资格 + 录制语音消息 + 发送消息 + 当前账号 + 服务器 + 用户 + 安卓版本 + 应用 + 应用的名称 + 电池设置 + 设备 + 电话 + 外部 + 内部 + 无效的密码 + 服务器目前处于维护模式 + 应用版本已过期 + 应用版本过低,不再被服务器支持,请更新 + 更新 + 你想重新授权还是删除此帐户? + 将此媒体保存到存储空间中将允许您设备上的所有其他应用访问 + 是否继续? + + 保存到存储空间中? + + 无法获取显示名,中止 + 无法储存显示名称,正在终止 + 编辑 + 编辑 + 编辑消息 + 8小时 + 4 星期 + 关闭 + 1 天 + 1小时 + 1 周 + 聊天内容过期 + 可指定聊天消息在一定时间后过期。注意:聊天中分享的文件不会被从所有者一方删除,但是会被从对话中取消分享。 + 获取网络设置失败 + 接受 + 拒绝 + 没有待处理的邀请 + 返回 + 需要文件访问权限 + 使用公共链接的用户 + 您:%1$s + 转发 + 转发至 … + 相册 + 还没有服务器吗?\n点此查看供应商 + 获取源代码 + 群组 + 访客 + 访客访问 + 无法启用/停用访客访问。 + 允许访客分享加入此对话的公开链接。 + 允许来宾 + 输入密码 + 访客访问密码 + 设置/禁用密码时发生错误。 + 设置密码来限制谁可以使用公共链接。 + 密码保护 + 重发邀请 + 由于错误,未发送邀请。 + 邀请函已再次发出。 + 分享对话链接 + 输入消息…… + 重要对话 + 重要对话忽略“勿扰”用户状态 + 邀请函 + 加入公开对话 + 保留 + 在您离开对话之前需要推举一位新的主持人 + %1$s 我上一次更改: %2$s + 离开对话 + 正在离开通话 ... + GNU 通用公共许可证,第3版 + 许可证 + 已达到%s字符限制 + 休息室 + 此会议预定开始时间 %1$s + 会议不久后将开始 + 您当前正在休息室等待。 + 你当前位置 + 需要位置权限 + 位置未知 + 已锁定 + 轻按解锁 + 未设置 + 标为已读 + 标为未读 + 失败 + 消息发送失败: + 取消回复 + 消息已读 + 消息已发送 + 使用语音通话需要“麦克风”权限 + 你错过了来自 %s 的一个通话 + 主持人 + 新的对话 + 可见性 + 未读的提及 + 未读消息 + %1$s不可用(尚未安装或被管理员限制使用) + 来宾 + + 没有公开的对话 + 没有可以加入的公开对话 \n可能是没有公开对话,或是您已经加入全部的公开对话 + 无代理 + 您没有开启语音的权限! + 您没有开启视频的权限! + 以后再说 + %1$s在%2$s通知频道中 + 通话 + 来电通知 + 消息 + 收到消息时通知 + 上传 + 上传进度通知 + 通知设置 + 始终提醒 + 当被提及时提醒 + 从不提醒 + 当前离线,请检查您的连接 + OK + 向注册用户开放对话 + 同样对访客用户开放 + 所有者 + 参与者 + 添加参与者 + 密码 + 设置权限 + 部分权限被拒绝 + 请允许权限 + 打开设置 + 请在 “设置” > “权限” 中开启权限 + 未找到账号 + 通过 %s 聊天 + 将麦克风静音 + 启用麦克风 + 消息 + 隐私 + 个人信息 + 设置为主持人 + 公开对话 + 通知推送已禁用 + 按键讲话 + 禁用麦克风的情况下,点击&并按住使用“按键讲话”功能 + 以后提醒我 + 取消收藏 + 移除群组和成员 + 移除参与者 + 移除团队和成员 + 重命名对话 + 重命名 + 回复 + 私下回复 + 保存 + 已成功保存 + 30秒 + 5 分钟 + 1分钟 + 10 分钟 + 600 + 60 + 30 + 300 + 搜索 + 清除搜索 + 选择一个账号 + 敏感对话 + 将在对话列表和通知中禁用消息预览 + %1$s发送一个GIF图 + 你已发送一张GIF图 + %1$s发送一个视频 + 你已发送一个视频 + %1$s发送一段音频 + 你已发送一段音频 + %1$s发送一张图片 + 你已发送一张图片 + 测试服务端连接 + 请升级您的%1$s 数据库 + 选择的账号导入失败 + 当你在浏览器中打开 %1$s web 界面时显示的链接 + 从 %1$s 应用导入账号 + 导入账号 + 从 %1$s 应用导入账号 + 导入账号 + 请将您的 %1$s 退出维护模式 + 请完成您的 %1$s 安装 + 测试连接中 + 服务器没有安装受支持的通话应用程序 + 服务器地址 https://… + %1$s 仅支持 %2$s 13 及更高版本 + 设置新密码 + 设置 + 已经更新了您现有的账号,而不是添加新的账号 + 高级 + 外观 + 通话 + 要求输入法禁用个性化学习(不保证键盘会执行) + 无痕键盘 + 无声音 + 您进行身份验证的服务器上未安装通话应用 + 通知 + 消息 + 根据电话号码匹配联系人,以集成通话应用快捷方式到系统联系人应用 + 错误 429 请求太多 + 你可以设置你的电话号码,这样其他用户能够找到你 + 输入电话号码 + 无效电话号码 + 成功设置电话号码 + 电话号码 + 电话号码集成 + 隐私 + 代理主机 + 代理密码 + 代理端口 + 代理类型 + 代理用户名 + 分享我的消息读取状态,并显示其他人的消息读取状态 + 消息读取状态 + 重新授权账号 + 移除 + 移除账号 + 请确认您真要移除当前账号。 + 使用安卓屏幕锁或受支持的生物统计学方法来锁定 %1$s + 屏幕锁不活动超时时间 + 屏幕锁 + 阻止在最近列表和应用中截屏 + 屏幕安全 + 服务器版本很旧,下一个版本将不受支持! + 服务器版本太旧,不受这个版本的安卓应用程序支持 + 不受支持的服务器 + 深色主题 + 系统默认 + 主题 + 浅色主题 + 主题 + 展示我的输入状态并显示其他人的输入状态 + 输入状态仅在使用高性能后端(HPB)时可用 + 输入状态 + 代理需要凭据 + 警告 + 仅当前账号可以重新授权 + 分享联系人 + 读取联系人权限是必需的 + 分享当前位置 + 共享链接 + 分享位置 + 分享这个位置 + 选择账号 + 已分享项目 + 看板卡片 + 图片、文件、语音消息 + 沒有已分享的项目 + 位置 + 分享的位置 + 排序依据 + 开始时间 + 切换账号 + 团队 + 选择文件 + 将这些文件发送到 %1$s? + 将此文件发送到 %1$s? + 抱歉,上传失败 + %1$s 上传失败 + 失败 + 分享来源:%1$s + 从设备上传 + 上传中 + %1$s 至 %2$s - %3$s\%% + 拍照 + 拍摄视频 + 用户 + 来自 %1$s的视频录制 + 来自%1$s的通话录制(%2$s) + 按住录音,松开发送 + 需要音频录制权限 + «滑动取消 + 网络研讨会 + 是的 + 下周 + 由于缺少权限,没有电话集成 + 仅被@提及的消息 + + 默认 + 遵循对话设置 + 1小时 + 在线 + 在线状态 + 开启对话 + 在 Files 应用中打开 + 转到帖子 + 播放/暂停语音消息 + 添加选项 + 编辑投票 + 结束投票 + 你确定要结束此投票吗?本操作无法撤销。 + 您无法为此投票选择更多选项。 + 多选答案 + 选项 + 私密投票 + 问题 + 你的问题 + 结果 + 设置 + 投票 + 投票已提交 + 先前设定 + 举起手 + 所有 + 如果没有权限,不能分享来自存储的文件 + 最近的帖子 + 通话正在被录制 + 取消录制开始 + 录制失败。请联系您的管理员。 + 开始录制 + 您想要停止录制吗? + 停止通话录制 + 停止录制 + 正在停止录制…… + 所有通话都需要同意才可录制 + 录制内容可能包括您的声音、摄像头的画面,以及屏幕分享的内容。要加入通话,您需要同意录制。您是否同意? + 在此对话中加入通话前要求准许录制 + 录制许可 + 通话可能被录制 + 录音 + 对话%1$s已从收藏中移除 + 对话%1$s已重命名 + 重置状态 + 在通话时无法加入其他房间 + 保存 + Scan QR Code + 只同步到受信任的服务器 + 联合云 + 仅对该实例用户和来宾可见 + 本地 + 只对通过手机上 Talk 应用的电话号码集成功能匹配的人可见 + 私有 + 同步到受信任的服务器、全局和公共地址簿 + 已发布 + 范围切换 + 更改 %1$s 的隐私级别 + 滚动到底部 + 几秒前 + 已选中 + 发送邮件 + 发送到 + 你不能在此聊天中分享内容 + 发送到 … + 发送而不通知 + 设置 + 从摄像头设置头像 + 设定状态 + 设置状态消息 + 分享 + 音频 + 文件 + 媒体文件 + 其他 + 投票 + 通话录制 + 语音 + 收藏 + 您没有开始通话的权限。 + 创建帖子 + 发起了通话 + 状态消息 + 切换到分组讨论 + 切换到主房间 + 拍张照片 + 拍照时出错 + 无权限应用无法拍照 + 重新拍照 + 发送 + 切换相机 + 裁剪相片 + 削减图片尺寸 + 切换闪光灯 + 30 分钟 + 本周 + 这是一个测试消息 + 本周末 + 帖子通知 + 帖子‌标题 + 今天 + 明天 + 翻译 + 翻译 + 复制已翻译的文字 + 检测语言 + 设备设置 + 无法检测语言 + 翻译失败 + 来自 + + 以及另外1个人正在输入... + 正在输入... + 正在输入... + 以及另外 %1$s 个人正在输入... + 取消归档对话 + 解除封禁 + 未读 + 从设备上传新头像 + 接替者: + 用户头像 + 地址 + 全名 + 电子邮件 + 电话号码 + Twitter + 网站 + 状态 + 检索个人用户信息失败 + 未设置个人信息 + 在你的个人资料页上添加姓名、图片和联系方式。 + 你什么状态? + + %d 票 + + diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000..61c45b6 --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -0,0 +1,723 @@ + + + 編輯 + 添加 + 添加到筆記 + 添加了對話 %1$s 至最愛 + %s內搜尋 + 顯示為離線 + 封存對話 + 對話封存後,預設會隱藏。選取篩選條件「已封存」可檢視已封存的對話。仍會收到直接提及。 + 已封存 + 已封存 %1$s + 音頻通話 + 藍牙 + 音頻輸出 + 電話 + 喇叭 + 有線耳機 + 您的狀態是自動設置的 + 虛擬化身大頭照 + 不在 + 返回按鈕 + 封禁 + 封禁參與者 + 封禁清單 + 忙碌 + 日曆 + 進階通話選項 + 請注意,通話已經持續一個小時了。 + 通話而不通知 + 已授予相機權限。請重新選擇相機。 + 取消登入 + 從雲中選擇虛擬化身大頭照 + 清除狀態訊息 + 繼此之後清空狀態訊息 + 關閉 + 關閉圖示 + 連線已建立 + 無法連接到伺服器 + 連線已中斷 - 已發送的訊息已排隊等待傳送 + 鎖定錄音以連續錄製音頻訊息 + 對話已封存 + 對話是唯讀 + 設定對話唯讀失敗 + 對話 + 建立對話 + 創建問題 + 自訂 + 危險地帶 + %2$s 中的 %1$s + 刪除虛擬化身大頭照 + 刪除語音錄音 + 已刪除對話 %1$s + 請勿打擾 + 不要清除 + 編輯 + 無法編輯超過24小時的訊息 + 編輯訊息 + 最新 + 已加密 + 結束通話 + 為所有人結束通話 + 載入您的聊天記錄時時發生問題 + 取消封禁參與者時發生錯誤 + 儲存 %1$s 失敗 + 15 分鐘 + 資料夾 + 加載中 … + %1$s(%2$d) + 關注的討論串 + 4 小時 + 無法擷取待定的邀請 + (已編輯) + 內部備註 + 隱藏 + 無法擷取語言 + 擷取失敗 + 今日稍後 + 離開通話 + 你離開了對話 %1$s + 正在載入更多結果 + 本地時間︰%1$s + 位置權限被拒絕 + 請在「應用程式設定」中啟用 + 已停用位置服務 + 您必須啟用位置服務 (GPS) 以使用此功能 + 鎖定對話 + 鎖符號 + 放下手 + 將對話 %1$s 標記為已讀 + 將對話 %1$s 標記為未讀 + 已提及 + 最新先 + 最舊先 + A - Z + Z - A + 最大先 + 最小先 + 已複製訊息 + 您確定要刪除此訊息? + 訊息被您刪除 + 由 %1$s 編輯 + 點按即可打開民意調查 + 無搜尋結果 + 輸入文字以搜尋 … + 搜尋 … + 訊息 + 所有通知靜音 + 所選帳戶現已匯入並可用 + 關於 + 活躍用戶 + 新增帳戶 + 此賬號已經被排定要刪除,也不能被修改。 + 打開主選項單 + 添加附件 + 添加 emoji + 添加到對話 + 添加參與者 + 加到我的最愛 + 完成 + 密碼:%1$s + 解鎖 %1$s + 要啟用藍牙喇叭,請授予「鄰近裝置」權限。 + 以視像通話方式接聽 + 僅以音頻通話方式接聽 + 更改音頻輸出 + 切換相機 + 掛斷 + 切換米高風 + 打開畫中畫模式 + 切換到自己的視頻 + 來電 + 對話名稱 + 通話通告 + %1$s 舉手 + 正在重新連線 … + 鈴聲響 + %1$s 通話中 + %1$s 電話對話中 + %1$s 視像對話中 + 在45秒內無回應,請點按以重試 + %s 通話 + %s 視像通話 + %s 音頻通話 + 要啟用影像通訊,請授予「相機」權限。 + 取消 + 無法獲取功能,正在中止 + 說明 + 您是否信任 %1$s 為 %2$s 發行的,有效期從 %3$s 到 %4$s,迄今不詳的SSL證書? + 請檢查憑證 + 您的SSL設定阻擋這連線。 + 更改驗證證書 + 變更密碼 + 取消編輯 + 取消編輯 + 刪除所有訊息 + 已刪除所有訊息 + 您確定要刪除對話內所有的訊息嗎? + 更改客戶端證書 + 設置客戶端證書 + + 複製 + 已複製到剪貼板 + 創建 + 停用 + 撤銷 + 發生錯誤了:( + 更多選項 + 設定 + 略過 + 不詳 + 選擇驗證證書 + 連線中 … + 完成 + 對話描述 + 對話資訊 + 視訊通話 + 音頻通話 + 未找到對話 + 對話設定 + 加入或者開一個新的對話 + 跟大家打招呼 + 複製 + 創建新對話 + 創建民意調查 + 您: + 今日 + 昨日 + 刪除 + 全部刪除 + 刪除對話 + 如果您刪除了此對話,它也將會從所有其他參與者處刪除。 + 刪除訊息 + 消息已成功刪除,但可能已分發到其他服務 + 立刻刪除 + 用户 %1$s 已被移除 + 從主持人降級 + 錄製話音短訊 + 傳送訊息 + 目前帳戶 + 伺服器 + 是否已安裝伺服器通知應用程式? + 用戶 + 是否已啟用用戶狀態? + Android 版本 + 應用程式 + 應用程式名稱 + 已註冊用戶 + 應用程式版本 + 略過電池優化,所有都好 + 電池最佳化已啟用,這可能會造成問題。您應該停用電池最佳化! + 電池設定 + 裝置 + 打開排憂解難清單: + 打開診斷螢幕 + 打開 dontkillmyapp.com + 最新的 firebase 推送權杖擷取 + 最新的 firebase 推送權杖産生 + 沒有設置 Firebase 推送權杖。請創建一個錯誤報告。 + Firebase 推送權杖 + Google Play 服務不可用。不支援通知功能。 + Google Play 服務 + Google Play 服務可用 + 在推送代理伺服器上的最新推送註冊 + 尚未在推送代理伺服器上進行註冊 + 在伺服器上的最新推送註冊 + 尚未在伺服器上進行註冊 + 元資訊 + 生成系統報告 + 來電通知頻道已啟用?? + 訊息通知頻道已啟用? + 通知權限 + 電話 + 伺服器 Talk 版本 + 伺服器版本 + 外部 + 內部 + Signaling 模式 + 無效的密碼 + 伺服器現正處於維護模式。 + 應用程式已過時 + 該應用程式太舊,不再受此伺服器支持。請更新。 + 更新 + 您要重新授權或刪除此帳戶嗎? + 將此媒體儲存到儲存空間中將允許您裝置上的任何其他應用程式存取它。 + 繼續? + + 保存到儲存裝置中? + + 無法取得顯示名稱,將停止操作。 + 無法儲存顯示名稱,操作中斷。 + 編輯 + 編輯 + 編輯訊息 + 編輯由管理員完成 + 活動對話選項單 + 預定 + 8 小时 + 4 個星期 + 關閉 + 1 日 + 1 小時 + 1 星期 + 刪除聊天訊息 + 聊天訊息可能會在一定時間後過期。注意:在聊天中共享的檔案不會為所有者刪除,但將不再在對話中共享。 + 擷取網絡設定失敗 + 接受 + 不批准 + 從 %1$s 到 %2$s + 沒有擱置的邀請 + 您有擱置的邀請 + 返回 + 需要檔案存取權限 + 篩選對話 + 用戶關注公共連結 + 你: %1$s + 轉寄 + 轉寄給 … + 相簿 + 您沒有自己的伺服器嗎?\n點這裡向服務供應商購買 + 取得原始碼 + 群組 + 訪客 + 訪客存取 + 無法啟用/禁用訪客存取。 + 允許客人分享公共連結以加入此對話。 + 允許訪客 + 輸入密碼 + 訪客存取密碼 + 設置/禁用密碼時出錯。 + 設置密碼以限制誰可以使用公共連結。 + 密碼保護 + 重新傳送邀請 + 由於錯誤,未發送邀請。 + 邀請函再次發出。 + 分享對話連結 + 輸入訊息 … + 電池優化未被忽略。為確保通知在背景中正常運作,請更改設定。請點擊「確定」並選擇「所有應用程式」-> %1$s -> 不優化。 + 不用理會電池優化 + 重要對話 + 「勿擾」用戶狀態將在重要對話中略過。 + 時間無效 + 邀請 + 加入公開對話 + 保留 + 在您離開對話之前您需要推舉一位新的主持人 + %1$s | 上次修改時間:%2$s + 結束對話 + 正在離開通話 … + GNU General Public License, 第3版 + 授權 + 已達到%s個字符的限制 + 等候室 + 此次會議預定開始時間為 %1$s + 會議即將開始 + 您目前正在等候室等候。 + 您目前的位置 + 需要位置權限 + 位置不詳 + 上鎖 + 點擊可解除鎖定 + 未定 + 標為已讀 + 標為未讀 + 對話已標記為重要 + 對話已標記為非敏感 + 對話已標記為敏感 + 對話已標記為非重要 + 會議已結束 + 訊息已新增至筆記 + 失敗了 + 傳送訊息失敗︰ + 離線 + 取消回覆 + 訊息已讀 + 正在發送 + 訊息已傳送 + 麥克風已啟用,音頻正在錄音 + 要啟用音頻通訊,請授予「米高風」權限。 + 您錯過了 %s 的來電 + 主持人 + 新對話 + 可見性 + 未讀的提及 + 未讀郵件 + %1$s 不可用(管理員未安裝或限制) + 訪客 + + 沒有公開的對話 + 沒有您可以加入的公開對話。\n可能是沒有公開的對話,或是您已經加入全部的公開對話。 + 無proxy代理 + 你無權啟動音頻! + 你無權啟動視像! + 現在不了 + %1$s在%2$s的通知頻道 + 通話 + 來電通知 + 訊息 + 來訊通知 + 上傳 + 通知上傳進度 + 通知設定 + 通知設定不正確 + 通知權限和電池設定已正確設置以接收通知。如果您仍然無法接收通知,請檢查是否已啟用通話和訊息的通知通道。您可以在 DontKillMyApp.com 或故障排除檢查清單中找到進一步的幫助。如果這些方法無法解決問題,請轉到診斷畫面並發送錯誤報告。 + 解決通知問題 + 一律通知 + 當有人提及你時通知 + 從不通知 + 目前離線,請檢查您的連接 + 確定 + 正在進行的會議 + 開放對話與註冊用戶 + 也向使用應用程式的來賓用戶開放 + 擁有者 + 參與者 + 添加參與者 + 密碼 + 設置權限 + 部份權限被拒絕。 + 請允許權限 + 開啟設定 + 請在「設定」→「權限」中授予權限 + 找不到帳戶 + 透過 %s 聊天 + 靜音米高風 + 啟用米高風 + 訊息 + 私隱條款 + 個人資訊 + 晉升為主持人 + 公開對話 + 取消推送通知 + 抱歉,出現問題,錯誤是 %1$s + 抱歉,出現問題,無法獲取測試推送訊息 + 推送通知已成功發送。您現在應該在此設備上收到標題為「測試推送通知」的通知 + 一鍵通 + 在停用米高風的情況下,單擊並按住即可使用一鍵通 + 稍後提醒我 + 取消我的最愛 + 移除群組及組員 + 移除參與者 + 移除密碼 + 移除團隊和成員 + 重新命名對話 + 重新命名 + 回覆 + 私下回覆 + 聊天室已成功保留 + 保存 + 保存成功 + 30秒 + 5分鐘 + 一分鐘 + 10分鐘 + 即時 + 600 + 60 + 30 + 300 + 搜尋 + 清除搜尋 + 選擇一個賬號 + 更新訊息 + 發送語音錄音 + 敏感對話 + 對話清單和通知中的訊息預覽將被停用。 + %1$s 傳送一個 GIF。 + 您傳送了一個 GIF 檔 + %1$s 傳送一個影片 + 您傳送了一個影片 + %1$s 傳送一個聲音檔 + 您傳送了一個聲音檔 + %1$s 傳送一個圖片 + 你傳送了一張圖片 + %1$s 傳送了一張卡片 + 測試伺服器連線 + 請升級你的%1$s數據庫 + 無法匯入所選的帳戶 + 在瀏覽器中打開 %1$s 網絡界面的連結。 + 從%1$s應用程式匯入帳戶 + 匯入帳戶 + 從%1$s應用程式匯入帳戶 + 匯入帳戶 + 撤銷您的%1$s退出維護模式 + 請將%1$s完整安裝 + 測試連線 + 伺服器未安裝受支持的 Talk 應用程式 + 伺服器網址 https://… + %1$s!只能在%2$s13版以上運作 + 設置新密碼 + 設置密碼 + 設定 + 已經更新了您現有的帳戶,而不是添加新的帳戶 + 進階 + 外觀 + 通話 + 請聯絡以下服務的管理員: + 打開診斷畫面以檢查設定或建立錯誤報告 + 診斷 + 指示鍵盤禁用個性化學習(無保證) + 隱身鍵盤 + 無聲 + 您進行身分驗證的伺服器上未安裝 Talk 應用程式 + 通知 + 通知被婉拒 + 已授予通知功能 + 訊息 + 根據電話號碼匹配聯絡人以將 Talk 快捷方式集成到電話簿中 + 錯誤 429 太多要求 + 設置您的電話號碼,以便其他用戶可以找到您 + 輸入電話號碼 + 電話號碼無效 + 電話號碼設置成功 + 電話號碼 + 電話號碼整合 + 私隱 + proxy代理主機 + Proxy 代理伺服器密碼 + proxy連接埠 + proxy類型 + Proxy 代理伺服器用戶名 + 分享我的閱讀狀態並顯示其他人的閱讀狀態 + 閱讀狀態 + 重新授權帳戶 + 移除 + 移除賬號 + 請確認 是否要清除目前的賬號 + 使用Android螢幕鎖定或生物識別方法去鎖定%1$s + 螢幕鎖定不活動超時時間 + 螢幕鎖定 + 防止在最近列表和應用程序內截屏 + 螢幕安全 + 伺服器版本非常舊,下一發布將不再支持版本! + 伺服器版本太舊,此版本的Android應用程式不支持該伺服器版本 + 不支援伺服器 + 伺服器通知應用程式未安裝 + 由省電模式設置 + 深色 + 使用系統預設 + 佈景主題 + 淺色 + 佈景主題 + 分享我的打字狀態並顯示其他人的打字狀態 + 鍵入狀態僅在使用高性能後端 (HPB) 時可用 + 打字狀態 + 代理需要身份驗證 + 警告 + 只有特定賬號可以被重新授權 + 分享聯絡人 + 需要讀取聯絡人的權限 + 分享目前位置 + 分享連結 + 分享位置 + 分享此位置 + 選擇帳戶 + 分享項目 + 看板卡片 + 圖片、檔案、音頻訊息 … + 沒有已分享的項目 + 位置 + 分享了的位置 + 當通知未正確設置時,顯示常規警告。 + 顯示常規警告。 + 排序方式 + 開始群組聊天 + 開始時間 + 切換帳戶 + 團隊 + 測試推送通知 + 測試結果 + 於今日 %1$s + 於明天 %1$s + 選擇檔案 + 將這些檔案發送到 %1$s? + 將此檔案發送到 %1$s? + 對不起,上傳失敗 + 上傳失敗 %1$s + 失敗 + 分享來自 %1$s + 從裝置上傳 + 上傳中 + %1$s 至 %2$s - %3$s\%% + 拍照 + 拍攝視像 + 用戶 + 視像錄影來自 %1$s + 談話錄音來自 %1$s(%2$s) + 按住錄音,鬆開傳送。 + 需要錄音許可 + « 滑動取消 + 網絡研討會 + + 下星期 + 沒有已封存的對話 + 未儲存離線訊息 + 由於缺少權限而無法集成電話號碼 + 全部消息 + 僅被 @提及的消息 + 關閉 + 默認 + 跟隨對話設定 + 1 小時 + 在線 + 線上狀態 + 開放對話 + 在“檔案”應用程式中打開 + 開啟筆記 + 前往討論串 + 播放﹨暫停話音短訊 + 播放速度控制 + 添加選項 + 編輯選票 + 結束民意調查 + 你真的要結束這個民意調查嗎?這是無法撤消的。 + 您無法為此民意調查選擇更多選項。 + 多個答案 + 刪除選項 %1$d + 選項 %1$d + 選項 + 私人投票 + 問題 + 您的問題 + 結果 + 設定 + 投票 + 已提交投票 + 先前設定 + 無法讀取 QR 碼 + 舉手 + 全部 + 沒有權限就無法從存儲分享檔案 + 最近的討論串 + 通話正在錄音中 + 取消錄製開始 + 錄製失敗。請聯絡管理員。 + 開始錄音 + 你想停止錄音嗎? + 停止通話錄音 + 停止錄音 + 停止錄音 ... + 所有通話都需要獲得錄製同意 + 錄製可能包括您的聲音、視像和螢幕共享。在您加入通話之前,需要獲得您的同意。您同意嗎? + 在此次對話中,需要在加入通話前給予錄製同意 + 錄製同意 + 通話可能會被錄製。 + 錄製 + 從最愛移除了對話 %1$s + 對話 %1$s 被重新命名 + 重新發送 + 重設狀態 + 通話時無法加入其他聊天室 + 保存 + 掃描 QR 碼 + 僅同步至受信任的伺服器 + 已聯盟 + 僅對此安裝的人和來賓可見 + 近端 + 僅對透過 Talk 在手提電話上通過電話號碼整合進行匹配的人可見 + 私人 + 同步到受信任的伺服器以及全域與公開的通訊錄 + 已發佈 + 切換範圍 + 更改 %1$s 的私隱級別 + 滾動到底部 + 搜尋圖示 + 秒前 + 已選擇 + 傳送電子郵件 + 傳送到 + 您不能在此聊天中分享內容 + 傳送到 … + 傳送而不通知 + 設置 + 從相機設置虛擬化身大頭照 + 設定狀態 + 設定狀態訊息 + 分享 + 加入對話 %1$s於 %2$s + 音頻 + 檔案 + 多媒體 + 其它 + 民意調查 + 通話錄音 + 音頻 + 顯示封禁原因 + 顯示被封禁的參與者 + 我的最愛 + 你無權開始通話 + 創建討論串 + 發起了通話 + 狀態訊息 + 狀態已還原 + 切換到分組討論室 + 切換到主室 + 拍照 + 拍照時出錯 + 沒有權限就無法拍照 + 重新拍照 + 傳送 + 切換相機 + 裁剪照片 + 縮小圖像尺寸 + 切換 torch + 30 分鐘 + 本星期 + 此乃測試訊息 + 本週末 + 取消創建討論串 + 討論串通知 + 回覆 + 討論串標題 + 找不到討論串 + 今日 + 明日 + 翻譯 + 翻譯 + 複製已翻譯的文字 + 檢測語言 + 裝置設定 + 無法檢測語言 + 翻譯失敗 + + + 還有 1 個人正在打字 … + 正在打字 … + 正在打字 … + 還有 %1$s 個人正在打字 … + 解除封存對話 + 對話取消存檔後,默認會再次顯示。 + 已解除封存 %1$s + 解除封禁 + 未讀 + 從裝置上傳新虛擬化身大頭照 + %1$s 不在辦公室,可能不會回覆。 + %1$s 今日不在辦公室 + 取代︰ + 用戶虛擬化身大頭照 + 地址 + 全名 + 電郵地址 + 電話號碼 + Twitter + 網站 + 狀態 + 無法檢索個人用戶資訊。 + 未設定個人資訊 + 在您的個人檔案中新增名字、頭像和聯絡資訊 + 視頻通話 + 您目前的狀態是什麼呢? + + 檢視 %d 則類似的訊息 + + + 此對話如無人於%1$d天內互動,將會自動為所有人刪除。 + + + %d 個回覆 + + + %d 選票 + + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..16e4864 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,723 @@ + + + 編輯 + 新增 + 新增至筆記 + 已新增對話 %1$s 至最愛 + 在 %s內搜尋 + 顯示為離線 + 封存對話 + 對話封存後,預設會隱藏。選取篩選條件「已封存」可檢視已封存的對話。仍會收到直接提及。 + 已封存 + 已封存 %1$s + 音訊通話 + 藍牙 + 音訊輸入 + 電話 + 揚聲器 + 有線式頭戴耳機 + 您的狀態為自動設定 + 頭像 + 外出 + 上一頁按鈕 + 封鎖 + 封鎖參與者 + 封鎖清單 + 忙碌 + 日曆 + 進階通話選項 + 通話已經持續一個小時了。 + 通話而不通知 + 已獲取相機權限,請重新選擇相機或影像來源。 + 取消登入 + 從雲端選擇頭像 + 清空狀態訊息 + 在……之後清空狀態訊息 + 關閉 + 關閉按鈕 + 連線已建立 + 無法連線至伺服器 + 連線中斷 -傳送的訊息已排入佇列 + 鎖定錄音以連續錄製語音訊息 + 對話已封存 + 對話為唯讀 + 設定對話唯讀失敗 + 對話 + 建立對話 + 建立問題 + 自訂 + 危險區域 + %2$s 中的 %1$s + 刪除頭像 + 刪除語音錄音 + 已刪除對話 %1$s + 請勿打擾 + 不要清空 + 編輯 + 無法編輯超過24小時的訊息 + 編輯訊息 + 近期 + 已加密 + 結束通話 + 為所有人結束通話 + 在載入您的對話時出現錯誤 + 解除封鎖參與者時遇到錯誤 + 儲存 %1$s 失敗 + 15分鐘 + 資料夾 + 正在載入…… + %1$s (%2$d) + 已追蹤的討論串 + 4小時 + 擷取擱置中的邀請失敗 + (已編輯) + 內部筆記 + 隱藏 + 無法擷取語言 + 擷取失敗 + 今天稍後 + 離開通話 + 您離開了對話 %1$s + 載入更多結果 + 當地時間:%1$s + 位置權限被拒絕 + 請在應用程式設定中啟用 + 已停用位置服務 + 請啟用位置服務 (GPS) 以使用此功能 + 鎖定對話 + 上鎖符號 + 放手 + 將對話 %1$s 標記為已讀 + 將對話 %1$s 標記為未讀 + 已提及 + 新的在前 + 舊的在前 + A - Z + Z - A + 最大優先 + 最小優先 + 已複製訊息 + 您確定要刪除此訊息? + 訊息已被您刪除 + 由 %1$s 編輯 + 點按開啟投票 + 無搜尋結果 + 輸入文字以搜尋 … + 搜尋 … + 訊息 + 靜音所有通知 + 選擇已匯入且可用的帳號 + 關於 + 活躍使用者 + 新增帳號 + 此帳號已經被排定要刪除,也不能被修改。 + 開啟主功能表 + 加入附件 + 加入 emoji + 加入到對話 + 新增成員 + 加到我的最愛 + 完成 + 密碼:%1$s + 解鎖 %1$s + 要啟用藍牙喇叭,請授予「鄰近裝置」權限。 + 以視訊通話方式接聽 + 僅以語音通話方式接聽 + 變更聲音輸出 + 切換相機 + 掛斷 + 切換麥克風 + 開啟畫中畫模式 + 切換到自己的視訊 + 來電 + 對話名稱 + 通話通告 + %1$s 舉手 + 正在重新連線 … + 鈴響聲 + %1$s 通話中 + %1$s 通過手機 + %1$s 視訊對話中 + 在45秒內無回應,請點按以重試 + %s 通話 + %s 視訊通話 + %s 語音通話 + 要啟用影像通訊,請授予「相機」權限。 + 取消 + 無法獲取功能,正在中止 + 字幕 + 您是否信任 %1$s 為 %2$s 發行的,有效期從 %3$s 到 %4$s,迄今不詳的 SSL 憑證? + 請檢查憑證 + 您的SSL設定阻擋這連線。 + 變更驗證憑證 + 變更密碼 + 取消編輯 + 取消編輯 + 刪除所有訊息 + 已刪除所有訊息 + 您確定要刪除對話內所有的訊息嗎? + 變更客戶端憑證 + 設定客戶端憑證 + + 複製 + 已複製至剪貼簿 + 建立 + 已停用 + 取消 + 抱歉,發生了一些問題! + 更多選項 + 設定 + 跳過 + 未知 + 選擇驗證憑證 + 連線中 … + 完成 + 對話描述 + 對話資訊 + 視訊通話 + 語音通話 + 找不到對話 + 對話設定 + 加入一個對話或者開一個新的 + 跟大家打個招呼! + 複製 + 建立新對話 + 建立調查活動 + 您: + 今天 + 昨天 + 刪除 + 全部刪除 + 刪除對話 + 如果您刪除了此對話,它也將會從所有其他參與者處刪除。 + 刪除訊息 + 訊息已成功刪除,但可能已分發到其他服務 + 立刻刪除 + 使用 %1$s 已移除 + 取消主持人資格 + 錄製語音訊息 + 傳送訊息 + 目前的帳號 + 伺服器 + 是否已安裝伺服器通知應用程式? + 使用者 + 已啟用使用者狀態? + Android 版本 + 應用程式 + 應用程式名稱 + 已註冊的使用者 + 應用程式版本 + 已忽略電池最佳化,一切都好 + 電池最佳化已啟用,這可能會造成問題。您應該停用電池最佳化! + 電池設定 + 裝置 + 開啟疑難排解檢查表 + 開啟診斷畫面 + 開啟 dontkillmyapp.com + 最新的 firebase 推播權杖擷取 + 最新的 firebase 推播權杖產生 + 並未設定 firebase 推播權杖。請建立臭蟲回報。 + Firebase 推播權杖 + Google Play 服務無法使用。不支援通知功能 + Google Play 服務 + Google Play 服務可用 + 在推播代理伺服器上的最新推播註冊 + 尚未在推播代理伺服器上進行註冊 + 在伺服器上的最新推播註冊 + 尚未在伺服器上註冊 + 詮釋資訊 + 產生系統報告 + 來電通知頻道已啟用? + 訊息通知頻道已啟用? + 通知權限 + 電話 + 伺服器 Talk 版本 + 伺服器版本 + 外部 + 內部 + Signaling 模式 + 無效的密碼 + 伺服器目前正處於維護模式。 + 應用程式已過時 + 應用程式太舊了,不再被此伺服器支援。請更新。 + 更新 + 您要重新授權或刪除此帳號嗎? + 將此媒體儲存到儲存空間中將允許您裝置上的任何其他應用程式存取它。 + 繼續? + + 儲存到儲存裝置中? + + 無法取得顯示名稱,將停止動作。 + 無法儲存顯示名稱,操作中斷。 + 編輯 + 編輯 + 編輯訊息 + 由管理員編輯 + 事件對話選單 + 安排 + 8小時 + 4 週 + 關閉 + 1天 + 1小時 + 1週 + 過期聊天訊息 + 聊天訊息可能會在一定時間後逾期。注意: 在聊天中分享的檔案不會為所有者刪除,但將不再於對話中被分享。 + 汲取訊號設定失敗 + 接受 + 回絕 + 從 %1$s 於 %2$s + 沒有待處理的邀請 + 您有待處理的邀請 + 返回 + 需要檔案存取權限 + 過濾對話 + 使用者追隨一個公開連結 + 你: %1$s + 轉貼 + 轉貼到… + 相簿 + 您沒有自己的伺服器嗎?\n點這裡向服務供應商購買 + 取得原始碼 + 群組 + 訪客 + 訪客存取 + 無法啟用/停用訪客存取。 + 允許訪客分享加入此對話的公開連結。 + 允許訪客 + 輸入密碼 + 訪客存取密碼 + 設定/停用密碼時發生錯誤。 + 設定密碼以限制誰可以使用公開連結。 + 密碼保護 + 重新傳送邀請 + 因為錯誤而無法傳送邀請。 + 邀請已再次傳送。 + 分享對話連結 + 輸入訊息 … + 未忽略電池最佳化。為確保通知在背景中正在運作,請變更設定!請點擊「確定」並選取「所有應用程式」→ %1$s → 無限制 + 忽略電池最佳化 + 重要交談 + 重要對話會忽略「請勿打擾」使用者狀態 + 無效的時間 + 邀請 + 加入開放對話 + 保留 + 在您離開對話前,您必須授權一位新的主持人 + %1$s | 上次修改時間:%2$s + 結束對話 + 正在離開通話 … + GNU產生公開授權,第3版 + 授權 + 已達 %s 個字元的限制 + 大廳 + 此會議預定於 %1$s 開始 + 會議即將開始 + 您目前正於大廳等候。 + 您目前的位置 + 需要位置權限 + 位置不明 + 已鎖定 + 點擊可解除鎖定 + 未設定 + 標為已讀 + 標為未讀 + 對話已標記為重要 + 對話已取消標記為敏感 + 對話已標記為敏感 + 對話已取消標記為重要 + 會議已結束 + 訊息已新增至筆記 + 失敗 + 傳送訊息失敗: + 離線 + 取消回覆 + 訊息已讀 + 正在傳送 + 已傳送郵件 + 已啟用麥克風,正在錄音 + 要啟用語音通訊,請授予「麥克風」權限。 + 您錯過了來自 %s 的通話 + 主持人 + 新對話 + 能見度 + 未讀的提及 + 未讀訊息 + %1$s 不可用(尚未安裝或被管理員限制使用) + 訪客 + + 沒有開放的對話 + 沒有您可以加入的開放對話。\n可能是沒有開放的對話,或是您已經加入全部的開放對話。 + 無proxy代理 + 您無權啟動語音! + 您無權啟動視訊! + 現在不要 + %1$s在%2$s的通知頻道 + 通話 + 來電通知 + 訊息 + 來訊通知 + 上傳 + 上傳進度通知 + 通知設定 + 通知設定不正確 + 通知權限與電池設定已正確設定以接收通知。若您仍然無法接收通知,請檢查是否已啟用通話與訊息的通知頻道。您可以在 DontKillMyApp.com 或疑難排解檢查表中找到進一步的說明。如果這些方法無法解決問題,請到診斷畫面並傳送錯誤回報。 + 通知疑難排解 + 總是通知 + 提及通知 + 從不通知 + 目前離線,請檢查您的連線 + 確定 + 正在進行的會議 + 向已註冊使用者開放對話 + 也向訪客應用程式的使用者開放 + 擁有者 + 參與者 + 新增成員 + 密碼 + 設定權限 + 部份權限被拒絕。 + 請允許權限 + 開啟設定 + 請在「設定」→「權限」中授予權限 + 找不到帳號 + 透過 %s 聊天 + 將麥克風靜音 + 啟用麥克風 + 訊息 + 隱私全條款 + 個人資訊 + 授予主持人資格 + 公開對話 + 取消推送通知 + 抱歉,發生了一點問題,錯誤為 %1$s + 抱歉,發生了一點問題,無法擷取測試推播訊息 + 已成功傳送推播通知。現在您應該會在此裝置上收到標題為「測試推播通知」的通知。 + 按住以說話 + 在停用麥克風的情況下,點擊並按住即可使用按住以說話 + 稍後提醒我 + 取消我的最愛 + 移除群組與成員 + 移除參與者 + 移除密碼 + 移除團隊與成員 + 重新命名對話 + 重新命名 + 回覆 + 私下回覆 + 聊天室已成功保留 + 儲存 + 已成功儲存 + 30秒 + 5分鐘 + 一分鐘 + 10分鐘 + 即時 + 600 + 60 + 30 + 300 + 搜尋 + 清除搜尋 + 選擇一個帳號 + 更新訊息 + 傳送語音錄音 + 敏感對話 + 對話清單與通知中的訊息預覽將停用 + %1$s 傳送一個 GIF. + 您傳送了一個 GIF 檔 + %1$s 傳送一個影片 + 您傳送了一個影片 + %1$s 傳送一個聲音檔 + 您傳送了一個聲音檔 + %1$s 傳送一個圖片 + 你傳送了一張圖片 + %1$s 傳送了一張卡片 + 測試伺服器連線 + 請升級你的%1$s資料庫 + 無法匯入所選的帳戶 + 在瀏覽器中開啟您 %1$s 網頁介面的連結。 + 從%1$s應用程式匯入帳戶 + 匯入帳戶 + 從%1$s應用程式匯入帳戶 + 匯入帳戶 + 請維護您的%1$s + 請將%1$s完整安裝 + 測試連線中 + 伺服器未安裝受支援的 Talk 應用程式 + 伺服器地址 https://… + %1$s!只能在%2$s13版以上運作 + 設定新密碼 + 設定密碼 + 設定 + 已更新您現有的帳號,而非新增帳號 + 進階 + 外觀 + 通話 + 請聯絡以下服務的管理員: + 開啟診斷畫面以檢查設定或建立錯誤回報 + 診斷 + 指示鍵盤停用個人化學習(不保證鍵盤會照做) + 無痕鍵盤 + 無聲 + 您嘗試進行身份驗證的伺服器並未安裝 Talk 應用程式 + 通知 + 通知被拒絕 + 已授予通知權限 + 訊息 + 根據電話號碼尋找相符的聯絡人以將 Talk 捷徑整合至系統聯絡人應用程式中 + 錯誤 429 太多請求 + 您可以設定電話號碼以讓其他使用者可以找到您 + 輸入電話號碼 + 無效的電話號碼 + 電話號碼設定成功 + 電話號碼 + 電話號碼整合 + 隱私 + proxy代理主機 + 代理伺服器密碼 + proxy連接埠 + proxy類型 + 代理伺服器使用者名稱 + 分享我的閱讀狀態並顯示其他人的閱讀狀態 + 訊息讀取狀態 + 重新授權帳號 + 移除 + 移除帳號 + 請確認 是否要清除目前的帳號 + 使用 Android 螢幕鎖定或支援的生物辨識方法鎖定 %1$s + 螢幕鎖定不活躍逾時 + 螢幕鎖定 + 避免在最近清單與應用程式內截圖 + 螢幕安全 + 伺服器版本非常舊,下一個版本將不支援! + 伺服器版本太舊了,此版本的 Android 應用程式不支援 + 不支援的伺服器 + 伺服器通知應用程式未安裝 + 由省電模式設定 + 暗色 + 使用系統預設值 + 佈景主題 + 亮色 + 佈景主題 + 分享我的打字狀態並顯示其他人的打字狀態 + 輸入狀態僅在使用高效能後端 (HPB) 時可用 + 輸入狀態 + 代理伺服器需要憑證 + 警告 + 只有特定帳號可以被重新授權 + 分享聯絡人 + 需要讀取聯絡人的權限 + 分享目前位置 + 分享連結 + 分享位置 + 分享地點 + 選擇帳號 + 已分享項目 + 看板卡片 + 圖片、檔案、語音訊息…… + 無已分享項目 + 路徑 + 共享的位置 + 通知未正確設定時,顯示一般警告 + 顯示一般通知警告 + 排序 + 開始群組聊天 + 開始時間 + 切換帳號 + 團隊 + 測試推播通知 + 測試結果 + 於今日的 %1$s + 於明天的 %1$s + 選擇檔案 + 傳送這些檔案給 %1$s? + 傳送檔案給 %1$s? + 抱歉,上傳失敗 + 上傳 %1$s 失敗 + 失敗 + 分享來源:%1$s + 從裝置上傳 + 上傳中… + %1$s 至 %2$s - %3$s\%% + 拍照 + 錄影 + 使用者 + 錄影來自 %1$s + Talk 錄音來自 %1$s (%2$s) + 按住以錄音,放開以傳送 + 需要錄音權限 + « 滑動取消 + 網路研討會 + + 下週 + 無封存的對話 + 未儲存離線訊息 + 因為缺少權限而無法整合電話號碼 + 所有訊息 + 僅被 @ - 提及 的訊息 + 關閉 + 預設 + 遵循對話設定 + 1小時 + 線上 + 線上狀態 + 開啟對話 + 在「檔案」應用程式中開啟 + 開啟筆記 + 到討論串 + 播放/暫停語音訊息 + 播放速度控制 + 新增選項 + 編輯投票 + 結束投票 + 您真的想要結束此投票嗎?此動作無法還原。 + 您無法為此投票選擇更多選項。 + 多個答案 + 刪除選項 %1$d + 選項 %1$d + 選項 + 私人投票 + 問題 + 您的問題 + 結果 + 設定 + 投票 + 投票已提交 + 先前設定 + 無法讀取 QR code + 舉手 + 全部 + 沒有權限就無法從儲存空間分享檔案 + 最近的討論串 + 通話正在錄製 + 取消錄音開始 + 錄音失敗。請聯絡您的管理員。 + 開始錄音 + 您想要停止錄製嗎? + 停止通話錄製 + 停止錄音 + 停止錄製…… + 所有通話都需要獲得錄製同意 + 錄製可能包含您的聲音、來自攝影機的影像與螢幕分享。在加入通話前,需要獲得您的同意。您同意嗎? + 在此對話中,需要在加入通話前給予錄製同意 + 同意錄製 + 通話可能會被錄製。 + 錄音 + 從最愛移除了對話 %1$s + 對話 %1$s 已重新命名 + 重新傳送 + 重設狀態 + 通話時無法加入其他聊天室 + 儲存 + 掃描 QR Code + 僅同步至受信任的伺服器 + 已聯盟 + 僅對此站台上的人們與訪客可見 + 本機 + 僅對在手機上的 Talk 透過電話號碼整合符合的人們可見 + 私人 + 同步到受信任的伺服器以及全域與公開的通訊錄 + 已發佈 + 切換範圍 + 變更 %1$s 的隱私層級 + 捲動至底部 + 搜尋圖示 + 秒前 + 已選擇 + 傳送電子郵件 + 傳送至 + 您不被允許在此聊天中分享內容 + 傳送至…… + 傳送而不通知 + 設定 + 從相機設定大頭照 + 設定狀態 + 設定狀態訊息 + 分享 + 加入對話 %1$s 於 %2$s + 音樂 + 檔案 + 多媒體 + 其他 + 投票 + 通話錄製 + 語音 + 顯示封鎖理由 + 顯示已封鎖的參與者 + 收藏 + 您不被允許開始通話 + 建立討論串 + 開始通話 + 狀態訊息 + 狀態已還原 + 切換至分組討論室 + 切換至主要聊天室 + 拍照 + 拍照時發生錯誤 + 沒有權限就無法拍照 + 重新拍照 + 傳送 + 切換相機 + 裁剪照片 + 縮小照片大小 + 切換閃光燈 + 30分鐘 + 這個禮拜 + 這是測試訊息 + 本週末 + 取消建立討論串 + 討論串通知 + 回覆 + 討論串標題 + 找不到討論串 + 今天 + 明天 + 翻譯 + 翻譯 + 複製已翻譯的文字 + 偵測語言 + 裝置設定 + 無法偵測語言 + 翻譯失敗 + + + 以及另外 1 個人正在輸入…… + 正在輸入…… + 正在輸入…… + 以及另外 %1$s 個人正在輸入…… + 解除封存對話 + 對話取消存檔後,預設會再次顯示。 + 已解除封存 %1$s + 取消封鎖 + 未讀 + 從裝置上傳新大頭照 + %1$s 不在辦公室,可能不會回應 + %1$s 今天不在辦公室 + 取代: + 使用者頭像 + 地址 + 全名 + 電子郵件 + 電話號碼 + Twitter + 網站 + 狀態 + 無法擷取個人使用者資訊。 + 未設定個人資訊 + 在您的個人檔案中新增名字、大頭照和聯絡資訊。 + 視訊連線 + 您目前的狀態是什麼呢? + + 檢視 %d 則類似的訊息 + + + 此對話若於%1$d天內沒有活動,將會自動為所有人刪除 + + + %d 個回覆 + + + %d 票 + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..d2f5c3c --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,83 @@ + + + + + @string/nc_no_proxy + HTTP + DIRECT + SOCKS + + + + @string/nc_notify_me_never + @string/nc_notify_me_mention + @string/nc_notify_me_always + + + + never + mention + always + + + + @string/nc_expire_message_off + @string/nc_expire_message_four_weeks + @string/nc_expire_message_one_week + @string/nc_expire_message_one_day + @string/nc_expire_message_eight_hours + @string/nc_expire_message_one_hour + + + + expire_0 + expire_2419200 + expire_604800 + expire_86400 + expire_28800 + expire_3600 + + + + @string/nc_screen_lock_timeout_immediate + @string/nc_screen_lock_timeout_30 + @string/nc_screen_lock_timeout_60 + @string/nc_screen_lock_timeout_300 + @string/nc_screen_lock_timeout_600 + + + + @string/nc_screen_lock_timeout_immediate + @string/nc_screen_lock_timeout_thirty + @string/nc_screen_lock_timeout_sixty + @string/nc_screen_lock_timeout_three_hundred + @string/nc_screen_lock_timeout_six_hundred + + + + 1 + 30 + 60 + 300 + 600 + + + + @string/nc_settings_theme_battery_saver + @string/nc_settings_theme_light + @string/nc_settings_theme_dark + + + + @string/nc_settings_theme_battery_saver_key + @string/nc_settings_theme_light_key + night_yes + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..558b00c --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/bool.xml b/app/src/main/res/values/bool.xml new file mode 100644 index 0000000..c971c43 --- /dev/null +++ b/app/src/main/res/values/bool.xml @@ -0,0 +1,11 @@ + + + + true + false + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..a39e44a --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,99 @@ + + + + #0082C9 + #006AA3 + @color/colorPrimary + #ff888888 + #ffffff + #B3FFFFFF + + + @android:color/white + #666666 + #A5A5A5 + + + #000000 + #de000000 + #99000000 + #61000000 + + + #deffffff + #99ffffff + + + #8Affffff + #8A000000 + + + #FFFFFF + + @color/high_emphasis_text + + #55FFFFFF + + + @color/high_emphasis_text + #DBDBDB + #222222 + + #D32F2F + #FF9800 + #006400 + #E8E8E8 + #757575 + #D5D5D5 + #000000 + #E9FFFFFF + #111111 + #767676 + + #666666 + #FFFFFF + + #FFFFFF + #99FFFFFF + #333333 + + #EFEFEF + #66EFEFEF + #AFAFAF + #D3CFCF + + #FFFFFF + #121212 + + #BF999999 + #FFCC00 + + + #D7D7D7 + #B4B4B4 + + + #606060 + + #99ffffff + + #99121212 + + #1F121212 + #EEEEEE + + + #FFFFFF + + #99000000 + #EF3B02 + #DBE2E9 + + diff --git a/app/src/main/res/values/defaults.xml b/app/src/main/res/values/defaults.xml new file mode 100644 index 0000000..b37be8a --- /dev/null +++ b/app/src/main/res/values/defaults.xml @@ -0,0 +1,10 @@ + + + + @string/nc_settings_theme_battery_saver_key + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..9f309e0 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,92 @@ + + + + + 16dp + + 72dp + 56dp + 16sp + 48dp + + 48dp + + + 8dp + 16dp + 40dp + 30dp + 96dp + 52dp + + 16sp + 14sp + 18dp + 12dp + 8dp + 18dp + + 120dp + 220dp + + 18sp + + 192dp + + 16dp + 24dp + 12dp + 16dp + 16sp + 16sp + 14sp + 56dp + 24dp + 32dp + 72dp + 72dp + 24dp + 32dp + 16dp + 8dp + 8dp + 400dp + 24dp + 18dp + + 110dp + 48dp + 48dp + 0dp + + 48dp + 4dp + 16sp + 48dp + 40dp + 2dp + 12dp + + 18dp + + 50dp + + 150dp + + 40dp + 30dp + 16dp + + 24dp + 24dp + 21dp + 12sp + + diff --git a/app/src/main/res/values/setup.xml b/app/src/main/res/values/setup.xml new file mode 100644 index 0000000..240ded6 --- /dev/null +++ b/app/src/main/res/values/setup.xml @@ -0,0 +1,59 @@ + + + + + HvAfHtAy/QdFYqAWFFXa1VV_Iv6ZQ1.tf5swMc^45wS_vz=Wm[oyRP5D- + nc + false + + Talk + Nextcloud Talk + Nextcloud + + https://push-notifications.nextcloud.com + + + false + false + true + + + + https://nextcloud.com/privacy/ + https://www.gnu.org/licenses/gpl-3.0.en.html + https://github.com/nextcloud/talk-android + https://github.com/nextcloud/talk-android/issues + https://nextcloud.com/providers + + + com.nextcloud.client + nextcloud + + + + + 829118773643-cq33cmhv7mnv7iq8mjv6rt7t15afc70k.apps.googleusercontent.com + https://nextcloud-a7dea.firebaseio.com + 829118773643 + AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s + 1:829118773643:android:54b65087c544d819 + nextcloud-a7dea + AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s + nextcloud-a7dea.appspot.com + + https://github.com/nextcloud/talk-android/blob/master/docs/notifications.md + https://dontkillmyapp.com/ + + + https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png + OpenStreetMap contributors + https://nominatim.openstreetmap.org/ + android@nextcloud.com + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..cc40c3c --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,905 @@ + + + + + + + Yes + No + and + Skip + Set + Dismiss + Sorry, something went wrong! + Create + Unknown + Disabled + Copy + Copied to clipboard + More options + + + Settings + Add + + + Test server connection + The link to your %1$s web interface when you open it in the browser. + Testing connection + Server does not have supported Talk app installed + + Server address https://… + Please finish your %1$s installation + Please upgrade your %1$s database + Please bring your %1$s out of maintenance + %1$s only works with %2$s 13 and up + Import account + Import accounts + Import account from the %1$s app + Import accounts from the %1$s app + Failed to import selected account + Selected account is now imported and available + Do you not have a server yet?\nClick here to get one from a provider + + + Push notifications disabled + Failed to fetch capabilities, aborting + Failed to fetch signaling settings + Display name couldn\'t be fetched, aborting + %1$s not available (not installed or restricted by admin) + Could not store display name, aborting + Sorry something went wrong, error is %1$s + Sorry something went wrong, cannot fetch test push message + + Search + Clear search + + Check out the certificate + Do you trust the until now unknown SSL certificate, issued by %1$s for %2$s, valid from %3$s to %4$s? + Your SSL setup prevented connection + + + Advanced + proxy_type + Proxy type + proxy_host + Proxy host + proxy_port + Proxy port + Proxy username + proxy_username + Proxy password + proxy_password + Proxy requires credentials + proxy_credentials + Reauthorize account + Set up client certificate + Change client certificate + Remove + Please confirm your intent to remove the current account. + Remove account + Unsupported server + The server version is too old and not supported by this version of the Android app + The server version is very old and will not be supported in the next release! + Warning + Only current account can be reauthorized + Talk app is not installed on the server you tried to authenticate against + Your already existing account was updated, instead of adding a new one + The account is scheduled for deletion, and cannot be changed + Notifications + Calls + call_ringtone + Messages + message_ringtone + Librem by feandesign + No sound + Server notifications app not installed + Please contact the administrator of + + Appearance + Theme + theme + battery_saver + follow_system + night_no + Use system default + Set by battery saver + Light + Dark + Privacy + Screen lock + Lock %1$s with Android screen lock or supported biometric method + screen_lock + Screen lock inactivity timeout + screen_lock_timeout + Screen security + Prevents screenshots in the recent list and inside the app + screen_security + Incognito keyboard + Instructs keyboard to disable personalized learning (without guarantees) + read_privacy + Tap to unlock + Locked + Share my read-status and show the read-status of others + Read status + Share my typing-status and show the typing-status of others + Typing status + Typing status is only available when using a high performance backend (HPB) + + 30 seconds + 1 minute + 5 minutes + 10 minutes + 30 + Immediate + 60 + 300 + 600 + + Unlock %1$s + Cancel + + No proxy + About + Privacy + Get source code + License + GNU General Public License, Version 3 + + Select an account + Add account + Active user + + Personal Info + + Diagnosis + Open diagnosis screen to check settings or create bug report + + Notifications are granted + Notifications are declined + + + Notification troubleshooting + Notification permission and battery settings are correctly set up to receive notifications. If you have problems to receive notifications anyway, please check if the notification channels for calls and messages are enabled. Further help can be found at DontKillMyApp.com or at the troubleshooting checklist. If this does not help, please go to diagnosis screen and send a bug report. + Open troubleshooting checklist + Open dontkillmyapp.com + Open diagnosis screen + + Ignore battery optimization + Battery optimization is not ignored. This should be changed to make sure that notifications work in the background! Please click OK and select \"All apps\" -> %1$s -> Do not optimize + Show regular notification warning + When notifications are not set up correctly, show a regular warning + Notifications are not set up correctly + Not now + + Meta information + Generation of system report + Phone + Device + Android version + App + App name + App version + Registered users + Google Play services + Google Play services are available + Google Play services are not available. Notifications are not supported + Battery settings + Battery optimization is enabled which might cause issues. You should disable battery optimization! + Battery optimization is ignored, all fine + Notification permissions + Calls notification channel enabled? + Messages notification channel enabled? + Firebase push token + Latest firebase push token generation + Latest firebase push token fetch + No firebase push token set. Please create a bug report. + Current account + Server + User + Server notification app installed? + User status enabled? + Latest push registration at server + Not yet registered at server + Latest push registration at push proxy + Not yet registered at push proxy + Server version + Server Talk version + Signaling Mode + Internal + External + Send email + Create issue + Build flavor + Test push notifications + Test results + Message copied + Push notification is sent successfully. You should now receive a notification on this device with the title \'Testing push notifications\' + + + Leave conversation + Delete all messages + Do you really want to delete all messages in this conversation? + All messages were deleted + Conversation marked as sensitive + Conversation unmarked as sensitive + Conversation marked as important + Conversation unmarked as important + Rename conversation + Rename + Delete conversation + + Schedule + Delete + Delete all + Ongoing meeting + Meeting ended + Invalid time + Today at %1$s + Tomorrow at %1$s + If you delete the conversation, it will also be deleted for all other participants. + + New conversation + Mark as read + Mark as unread + Add to favorites + Remove from favorites + Create a new conversation + Join open conversations + Open conversation to registered users + Also open to guest app users + Visibility + + Added conversation %1$s to favorites + Removed conversation %1$s from favorites + Marked conversation %1$s as unread + Marked conversation %1$s as read + Deleted conversation %1$s + You left the conversation %1$s + Conversation %1$s was renamed + + Forward to … + + + No open conversations + No open conversations that you can join.\nEither there are no open conversations or you already joined all of them. + + + Add participants + Done + User avatar + Back button + + Please allow permissions + Some permissions were denied. + Please grant permissions at Settings > Permissions + Open settings + Set permissions + To enable video communication please grant \"Camera\" permission. + To enable voice communication please grant \"Microphone\" permission. + To enable bluetooth speakers please grant \"Nearby devices\" permission. + Microphone is enabled and audio is recording + + + %s voice call + %s video call + %s call + INCOMING + RINGING + Connecting … + Guest + Public conversation + No response in 45 seconds, tap to try again + Reconnecting … + Currently offline, please check your connectivity + Leaving call … + %1$s in call + %1$s with phone + %1$s with video + You missed a call from %s + Open picture-in-picture mode + Change audio output + Toggle camera + Toggle microphone + Hang up + Answer as voice call only + Answer as video call + Switch to self video + %1$s raised the hand + Raise hand + Lower hand + It\'s not possible to join other rooms while being in a call + The call has been running for one hour. + End call for everyone + Leave call + End call + + + Mute microphone + Enable microphone + + + %1$s on %2$s notification channel + Calls + Messages + Uploads + Notify about incoming calls + Notify about incoming messages + Notify about upload progress + Notification settings + Messages + Always notify + Notify when mentioned + Never notify + Call notifications + Sensitive conversation + Message preview will be disabled in conversation list and notifications + Important conversation + \"Do not disturb\" user status is ignored for important conversations + + OK, all done! + OK + Conversation name + Create conversation + Add emojis + + + Push-to-talk + With microphone disabled, click&hold to use Push-to-talk + Select authentication certificate + Change authentication certificate + + + Demote from moderator + Promote to moderator + Remove participant + Remove team and members + Remove group and members + Pin: %1$s + + + Set status + Online status + Status message + What is your status? + Clear status message after + Clear status message + Set status message + Online + Do not disturb + Away + Busy + Invisible + + Mute all notifications + Appear offline + 😃 + 👍 + 👎 + ❤️ + 😯 + 😢 + 🙏 + 🔥 + More emojis + Don\'t clear + Today + 15 minutes + 30 minutes + 1 hour + 4 hours + This week + seconds ago + Reset status + + + Unread mentions + Conversations + Open conversations + Lock conversation + There was a problem loading your chats + Close + Close Icon + + + Enter a message … + Yesterday + Today + Voice call + Video call + Event conversation menu + Conversation info + Unread messages + %1$s sent a GIF. + %1$s sent an audio. + %1$s sent a video. + %1$s sent an image. + %1$s sent a deck card + You sent a GIF. + You sent an audio. + You sent a video. + You sent an image. + %1$s: %2$s + Cancel reply + + You: %1$s + Message read + Message sent + Message added to notes + Offline + Failed + Sending + Failed to send message: + Add attachment + Recent + You: + + See %d similar message + See %d similar messages + + Send voice recording + + + Guest access + Allow guests + Allow guests to share a public link to join this conversation. + Cannot enable/disable guest access. + Set Password + Password + Change Password + Remove Password + Set new password + Password protection + Set a password to restrict who can use the public link. + Guest access password + Enter a password + Error during setting/disabling the password. + Share conversation link + Resend invitations + Invitations were sent out again. + Invitations were not send due to an error. + Share link + + + Send message + + + Join a conversation or start a new one + Say hi to your friends and colleagues! + + + %s characters limit has been hit + Group + Team + Participants + Add participants + Start group chat + + Owner + Moderator + User + Guest + User following a public link + + + Back + %1$s | Last modified: %2$s + Sort by + file_browser_sort_by + sort_a_to_z + A - Z + Z - A + Newest first + Oldest first + Biggest first + Smallest first + + + Webinar + Lobby + Start time + You are currently waiting in the lobby. + This meeting is scheduled for %1$s + The meeting will start soon + Not set + + + Copy + Forward + Reply + Reply privately + + This conversation will be automatically deleted for everyone in %1$d day of no activity + This conversation will be automatically deleted for everyone in %1$d days of no activity + + Delete message + Delete now + Keep + Message deleted successfully, but it might have been leaked to other services + You are not allowed to start a call + You need to promote a new moderator before you can leave the conversation + Room is retained successfully + + Share + Send to + Send to … + Sharing files from storage is not possible without permissions + Open in Files app + You are not allowed to share content to this chat + + is typing … + are typing … + and 1 other is typing … + and %1$s others are typing … + %1$s in %2$s + + Go to thread + Create a thread + Thread title + Cancel thread creation + Recent threads + Followed threads + Reply + + %d reply + %d replies + + Thread notifications + No threads found + Default + Follow conversation settings + All messages + \@-mentions only + Off + + + Add to conversation + Take photo + Take video + Create poll + Share from %1$s + Sorry, upload failed + Choose files + Send these files to %1$s? + Send this file to %1$s? + Uploading + Upload from device + Gallery + + %1$s to %2$s - %3$s\%% + Failure + Failed to upload %1$s + Permission for file access is required + + + Video recording from %1$s + + + Share location + location permission is required + Share current location + Share this location + Shared location + Your current location + Position unknown + + + Share contact + Permission to read contacts is required + + + Choose account + + + Talk recording from %1$s (%2$s) + Hold to record, release to send. + Record voice message + « Slide to cancel + Play/pause voice message + Permission for audio recording is required + Playback speed control + Delete voice recording + + + Match contacts based on phone number to integrate Talk shortcut into system contacts app + Phone number integration + Phone number + You can set your phone number so other users will be able to find you + Invalid phone number + Phone number set successfully + Enter phone number + No phone number integration due to missing permissions + Chat via %s + Account not found + Edit + + + Save + Save to storage? + Saving this media to storage will allow any other apps on your device to access it. + Continue? + Yes + No + Saved successfully + + Favorite + Status + Encrypted + + Avatar + No personal info set + Add name, picture and contact details on your profile page. + Failed to retrieve personal user information. + Phone number + Email + Address + Website + Twitter + Full name + folder + Loading … + Edit + Save + Upload new avatar from device + Choose avatar from cloud + Delete avatar + Private + Only visible to people matched via phone number integration through Talk on mobile + Lock symbol + Local + Only visible to people on this instance and guests + Federated + Only synchronize to trusted servers + Published + Synchronize to trusted servers and the global and public address book + Scope toggle + Change privacy level of %1$s + + + Search in %s + + + 999+ + REPLYABLE_TAG + + Open main menu + Failed to save %1$s + Selected + %1$s (%2$d) + Invalid password + Do you want to reauthorize or delete this account? + User %1$s was removed + + App is outdated + The app is too old and no longer supported by this server. Please update. + Update + Switch account + Server is currently in maintenance mode. + + + Take a photo + Switch camera + Re-take photo + Toggle torch + Crop photo + Reduce image size + Send + Error taking picture + Taking a photo is not possible without permissions + Camera permission granted. Please choose camera again. + + + Bluetooth + Speaker + Phone + Audio output + Wired headset + + + Advanced call options + + + Start recording + Stop recording + Stop Call recording + Do you really want to stop the recording? + The call is being recorded + Cancel recording start + Stopping recording … + The recording failed. Please contact your administrator. + The call might be recorded. + The recording might include your voice, video from camera, and screen share. Your consent is required before joining the call. Do you consent? + Recording + Recording consent + Require recording consent before joining call in this conversation + Recording consent is required for all calls + + + Shared items + Images, files, voice messages … + No shared items + Media + File + Call recording + Audio + Voice + Other + Poll + Location + Deck card + + + Messages + Load more results + Search … + Start typing to search … + No search results + Search Icon + + + Tap to open poll + + %d vote + %d votes + + Add option + Edit vote + Vote + Vote submitted + End poll + Do you really want to end this poll? This cannot be undone. + You cannot vote with more options for this poll. + Results + Question + Your question + Options + Option %1$d + Delete option %1$d + Settings + Private poll + Multiple answers + + All + Send without notification + Call without notification + Set avatar from camera + Conversation description + + + Expire chat messages + Off + 4 weeks + 1 week + 1 day + 8 hours + 1 hour + Chat messages can be expired after a certain time. Note: Files shared in chat will not be deleted for the owner, but will no longer be shared in the conversation. + + + Switch to main room + Switch to breakout room + + + Invitations + from %1$s at %2$s + Accept + Reject + You have pending invitations + No pending invitations + + You are not allowed to activate audio! + You are not allowed to activate video! + Scroll to bottom + Translate + Translation + From + To + Detect language + Device settings + Translation failed + Could not detect language + Copy translated text + Danger zone + Filter conversations + Mentioned + Unread + 3128 + 8080 + 1080 + This is a test message + Lock recording for continuously recording of the voice message + Remind me later + Next week + This weekend + Tomorrow + Later today + Custom + Set + Calendar + Video call + Audio call + started a call + Error 429 Too Many Requests + Caption + Retrieval failed + Languages could not be retrieved + Edit message + Edit + Update message + Cancel editing + Messages older than 24 hours can not be edited + Conversation is read only + Edit message + (edited) + Conversation not found + Add to Notes + Edited by admin + Cancel editing + Edit + Failed to fetch pending invitations + Edited by %1$s + Join conversation %1$s at %2$s + Conversation settings + Ban participant + Show banned participants + Bans list + Connection lost - Sent messages are queued + Connection established + Message deleted by you + Unban + Internal note + Ban + Show ban reason + Error occurred when unbanning participant + No connection to server + Archive conversation + Unarchive conversation + Archived + Once a conversation is archived, it will be hidden by default. Select the filter \"Archived\" to view archived conversations. Direct mentions will still be received. + Once a conversation is unarchived, it will be shown by default again. + No offline messages saved + Previously set + Failed to set conversation Read-only + Status Reverted + Your status was set automatically + + %1$s is out of office and might not respond + %1$s is out of office today + Replacement: + Resend + No archived conversations + Archived %1$s + Unarchived %1$s + Conversation is archived + Local time: %1$s + Open Notes + Cancel Login + Scan QR Code + QR code could not be read + Are you sure you want to delete this message? + Location permission denied + Please enable it in the app settings + Location services disabled + Please enable location services (GPS) to use this feature + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..927e7b2 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/auth.xml b/app/src/main/res/xml/auth.xml new file mode 100644 index 0000000..88efd44 --- /dev/null +++ b/app/src/main/res/xml/auth.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/xml/backup_config.xml b/app/src/main/res/xml/backup_config.xml new file mode 100644 index 0000000..4be2fa9 --- /dev/null +++ b/app/src/main/res/xml/backup_config.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/xml/chip_others.xml b/app/src/main/res/xml/chip_others.xml new file mode 100644 index 0000000..5d2350f --- /dev/null +++ b/app/src/main/res/xml/chip_others.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/xml/chip_you.xml b/app/src/main/res/xml/chip_you.xml new file mode 100644 index 0000000..f38cc34 --- /dev/null +++ b/app/src/main/res/xml/chip_you.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/xml/contacts.xml b/app/src/main/res/xml/contacts.xml new file mode 100644 index 0000000..0670597 --- /dev/null +++ b/app/src/main/res/xml/contacts.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/xml/file_provider_paths.xml b/app/src/main/res/xml/file_provider_paths.xml new file mode 100644 index 0000000..5cab30c --- /dev/null +++ b/app/src/main/res/xml/file_provider_paths.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..6f9486e --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/syncadapter.xml b/app/src/main/res/xml/syncadapter.xml new file mode 100644 index 0000000..bab4e90 --- /dev/null +++ b/app/src/main/res/xml/syncadapter.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/qa/ic_launcher-web.png b/app/src/qa/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..9bde487a33852cf9a3f4aa34a19877bb64a003ca GIT binary patch literal 31078 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 GsQ&|^KBDyi literal 0 HcmV?d00001 diff --git a/app/src/qa/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java b/app/src/qa/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java new file mode 100644 index 0000000..94778d8 --- /dev/null +++ b/app/src/qa/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.java @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-FileCopyrightText: 2017-2018 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils; + + +import com.nextcloud.talk.interfaces.ClosedInterface; + +public class ClosedInterfaceImpl implements ClosedInterface { + @Override + public void providerInstallerInstallIfNeededAsync() { + // does absolutely nothing :) + } + + @Override + public boolean isGooglePlayServicesAvailable() { + return false; + } + + @Override + public void setUpPushTokenRegistration() { + // no push notifications for qa build flavour :( + } +} diff --git a/app/src/qa/res/drawable/ic_launcher_background.xml b/app/src/qa/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..72e5259 --- /dev/null +++ b/app/src/qa/res/drawable/ic_launcher_background.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/app/src/qa/res/drawable/ic_launcher_foreground.xml b/app/src/qa/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..e7047a2 --- /dev/null +++ b/app/src/qa/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/qa/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/qa/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..94a0963 --- /dev/null +++ b/app/src/qa/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/qa/res/mipmap-hdpi/ic_launcher.png b/app/src/qa/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..99024db8ebd23bbb5638ed52cea41fac902a102c GIT binary patch literal 5184 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 literal 0 HcmV?d00001 diff --git a/app/src/qa/res/mipmap-mdpi/ic_launcher.png b/app/src/qa/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2b6d24b1c0984fcea6fb0296eaf17e702a866cbd GIT binary patch literal 3229 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 P00000NkvXXu0mjf$`ckF literal 0 HcmV?d00001 diff --git a/app/src/qa/res/mipmap-xhdpi/ic_launcher.png b/app/src/qa/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a0528c11be638a176148ea733b196de14cb6cc33 GIT binary patch literal 7299 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�CBgoJ2bMP8?tyM_OV08d> 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 QdU!xtUR|zS#w_Un06%TK^#A|> literal 0 HcmV?d00001 diff --git a/app/src/qa/res/values/setup.xml b/app/src/qa/res/values/setup.xml new file mode 100644 index 0000000..baf2c6c --- /dev/null +++ b/app/src/qa/res/values/setup.xml @@ -0,0 +1,12 @@ + + + + Talk QA + Nextcloud Talk QA + Nextcloud + diff --git a/app/src/test/java/android/util/Log.kt b/app/src/test/java/android/util/Log.kt new file mode 100644 index 0000000..aa69a10 --- /dev/null +++ b/app/src/test/java/android/util/Log.kt @@ -0,0 +1,56 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package android.util + +/** + * Dummy implementation of android.util.Log to be used in unit tests. + * + * + * The Android Gradle plugin provides a library with the APIs of the Android framework that throws an exception if any + * of them are called. This class is loaded before that library and therefore becomes the implementation used during the + * tests, simply printing the messages to the system console. + */ +object Log { + + @JvmStatic + fun d(tag: String, msg: String): Int { + println("DEBUG: $tag: $msg") + + return 1 + } + + @JvmStatic + fun e(tag: String, msg: String): Int { + println("ERROR: $tag: $msg") + + return 1 + } + + @JvmStatic + fun i(tag: String, msg: String): Int { + println("INFO: $tag: $msg") + + return 1 + } + + @JvmStatic + fun isLoggable(tag: String?, level: Int): Boolean = true + + @JvmStatic + fun v(tag: String, msg: String): Int { + println("VERBOSE: $tag: $msg") + + return 1 + } + + @JvmStatic + fun w(tag: String, msg: String): Int { + println("WARN: $tag: $msg") + + return 1 + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/CallParticipantListExternalSignalingTest.java b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListExternalSignalingTest.java new file mode 100644 index 0000000..02b6dc7 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListExternalSignalingTest.java @@ -0,0 +1,654 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.models.json.participants.Participant; +import com.nextcloud.talk.signaling.SignalingMessageReceiver; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.InOrder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.DISCONNECTED; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.IN_CALL; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_AUDIO; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_VIDEO; +import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.GUEST; +import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.GUEST_MODERATOR; +import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.MODERATOR; +import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.OWNER; +import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.USER; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class CallParticipantListExternalSignalingTest { + + private final ParticipantsUpdateParticipantBuilder builder = new ParticipantsUpdateParticipantBuilder(); + + private CallParticipantList callParticipantList; + private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener; + + private CallParticipantList.Observer mockedCallParticipantListObserver; + + private Collection expectedJoined; + private Collection expectedUpdated; + private Collection expectedLeft; + private Collection expectedUnchanged; + + // The order of the left participants in some tests depends on how they are internally sorted by the map, so the + // list of left participants needs to be checked ignoring the sorting (or, rather, sorting by session ID as in + // expectedLeft). + // Other tests can just relay on the not guaranteed, but known internal sorting of the elements. + private final ArgumentMatcher> matchesExpectedLeftIgnoringOrder = left -> { + Collections.sort(left, Comparator.comparing(Participant::getSessionId)); + return expectedLeft.equals(left); + }; + + private static class ParticipantsUpdateParticipantBuilder { + private Participant newUser(long inCall, long lastPing, String sessionId, Participant.ParticipantType type, + String userId) { + Participant participant = new Participant(); + participant.setInCall(inCall); + participant.setLastPing(lastPing); + participant.setSessionId(sessionId); + participant.setType(type); + participant.setUserId(userId); + participant.setActorType(Participant.ActorType.USERS); + participant.setActorId(userId); + + return participant; + } + + private Participant newGuest(long inCall, long lastPing, String sessionId, Participant.ParticipantType type) { + Participant participant = new Participant(); + participant.setInCall(inCall); + participant.setLastPing(lastPing); + participant.setSessionId(sessionId); + participant.setType(type); + participant.setActorType(Participant.ActorType.GUESTS); + participant.setActorId("sha1-" + sessionId); + + return participant; + } + } + + @Before + public void setUp() { + SignalingMessageReceiver mockedSignalingMessageReceiver = mock(SignalingMessageReceiver.class); + + callParticipantList = new CallParticipantList(mockedSignalingMessageReceiver); + + mockedCallParticipantListObserver = mock(CallParticipantList.Observer.class); + + // Get internal ParticipantListMessageListener from callParticipantList set in the + // mockedSignalingMessageReceiver. + ArgumentCaptor participantListMessageListenerArgumentCaptor = + ArgumentCaptor.forClass(SignalingMessageReceiver.ParticipantListMessageListener.class); + + verify(mockedSignalingMessageReceiver).addListener(participantListMessageListenerArgumentCaptor.capture()); + + participantListMessageListener = participantListMessageListenerArgumentCaptor.getValue(); + + expectedJoined = new ArrayList<>(); + expectedUpdated = new ArrayList<>(); + expectedLeft = new ArrayList<>(); + expectedUnchanged = new ArrayList<>(); + } + + @Test + public void testParticipantsUpdateJoinRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateJoinRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", USER, "theUserId4")); + participants.add(builder.newUser(DISCONNECTED, 5, "theSessionId5", OWNER, "theUserId5")); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateJoinRoomThenJoinCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateJoinRoomThenJoinCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateJoinRoomAndCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateJoinRoomAndCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateJoinRoomAndCallRepeated() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + participantListMessageListener.onParticipantsUpdate(participants); + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateChangeCallFlags() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateChangeCallFlagsSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", USER, "theUserId4")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedUpdated.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedUpdated.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", USER, "theUserId4")); + expectedUnchanged.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2", GUEST)); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateChangeLastPing() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateChangeLastPingSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", USER, "theUserId3")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 108, "theSessionId2", GUEST)); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 815, "theSessionId3", USER, "theUserId3")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateChangeParticipantType() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", USER, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateChangeParticipantTypeeSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", USER, "theUserId3")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", USER, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2", GUEST_MODERATOR)); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", MODERATOR, "theUserId3")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateLeaveCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateLeaveCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateLeaveCallThenLeaveRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateLeaveCallThenLeaveRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateLeaveCallAndRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateLeaveCallAndRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated), + argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged)); + } + + @Test + public void testParticipantsUpdateSeveralEventsSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + participants.add(builder.newUser(IN_CALL, 5, "theSessionId5", OWNER, "theUserId5")); + // theSessionId6 has not joined yet. + participants.add(builder.newGuest(IN_CALL | WITH_VIDEO, 7, "theSessionId7", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 8, "theSessionId8", USER, "theUserId8")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 9, "theSessionId9", MODERATOR, "theUserId9")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + // theSessionId1 is gone. + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", OWNER, "theUserId5")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6", GUEST)); + participants.add(builder.newGuest(IN_CALL, 7, "theSessionId7", GUEST)); + participants.add(builder.newUser(IN_CALL, 8, "theSessionId8", USER, "theUserId8")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", USER, "theUserId9")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6", GUEST)); + expectedJoined.add(builder.newUser(IN_CALL, 8, "theSessionId8", USER, "theUserId8")); + expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", OWNER, "theUserId5")); + expectedUpdated.add(builder.newGuest(IN_CALL, 7, "theSessionId7", GUEST)); + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + // Last ping and participant type are not seen as changed, even if they did. + expectedUnchanged.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", USER, "theUserId9")); + + verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated), + argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged)); + } + + @Test + public void testAllParticipantsUpdateDisconnected() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + InOrder inOrder = inOrder(mockedCallParticipantListObserver); + + inOrder.verify(mockedCallParticipantListObserver).onCallEndedForAll(); + inOrder.verify(mockedCallParticipantListObserver).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testAllParticipantsUpdateDisconnectedWithSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newUser(DISCONNECTED, 2, "theSessionId2", USER, "theUserId2")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 4, "theSessionId4", GUEST)); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedLeft.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 4, "theSessionId4", GUEST)); + + InOrder inOrder = inOrder(mockedCallParticipantListObserver); + + inOrder.verify(mockedCallParticipantListObserver).onCallEndedForAll(); + inOrder.verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated), + argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged)); + } + + @Test + public void testAllParticipantsUpdateDisconnectedNoOneInCall() { + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED); + + InOrder inOrder = inOrder(mockedCallParticipantListObserver); + + inOrder.verify(mockedCallParticipantListObserver).onCallEndedForAll(); + verifyNoMoreInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testAllParticipantsUpdateDisconnectedThenJoinCallAgain() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/CallParticipantListInternalSignalingTest.java b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListInternalSignalingTest.java new file mode 100644 index 0000000..a93325f --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListInternalSignalingTest.java @@ -0,0 +1,528 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.models.json.participants.Participant; +import com.nextcloud.talk.signaling.SignalingMessageReceiver; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.DISCONNECTED; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.IN_CALL; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_AUDIO; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_VIDEO; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +public class CallParticipantListInternalSignalingTest { + + private final UsersInRoomParticipantBuilder builder = new UsersInRoomParticipantBuilder(); + + private CallParticipantList callParticipantList; + private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener; + + private CallParticipantList.Observer mockedCallParticipantListObserver; + + private Collection expectedJoined; + private Collection expectedUpdated; + private Collection expectedLeft; + private Collection expectedUnchanged; + + // The order of the left participants in some tests depends on how they are internally sorted by the map, so the + // list of left participants needs to be checked ignoring the sorting (or, rather, sorting by session ID as in + // expectedLeft). + // Other tests can just relay on the not guaranteed, but known internal sorting of the elements. + private final ArgumentMatcher> matchesExpectedLeftIgnoringOrder = left -> { + Collections.sort(left, Comparator.comparing(Participant::getSessionId)); + return expectedLeft.equals(left); + }; + + private static class UsersInRoomParticipantBuilder { + private Participant newUser(long inCall, long lastPing, String sessionId, String userId) { + Participant participant = new Participant(); + participant.setInCall(inCall); + participant.setLastPing(lastPing); + participant.setSessionId(sessionId); + participant.setUserId(userId); + participant.setActorType(Participant.ActorType.USERS); + participant.setActorId(userId); + + return participant; + } + + private Participant newGuest(long inCall, long lastPing, String sessionId) { + Participant participant = new Participant(); + participant.setInCall(inCall); + participant.setLastPing(lastPing); + participant.setSessionId(sessionId); + participant.setActorType(Participant.ActorType.GUESTS); + participant.setActorId("sha1-" + sessionId); + + return participant; + } + } + + @Before + public void setUp() { + SignalingMessageReceiver mockedSignalingMessageReceiver = mock(SignalingMessageReceiver.class); + + callParticipantList = new CallParticipantList(mockedSignalingMessageReceiver); + + mockedCallParticipantListObserver = mock(CallParticipantList.Observer.class); + + // Get internal ParticipantListMessageListener from callParticipantList set in the + // mockedSignalingMessageReceiver. + ArgumentCaptor participantListMessageListenerArgumentCaptor = + ArgumentCaptor.forClass(SignalingMessageReceiver.ParticipantListMessageListener.class); + + verify(mockedSignalingMessageReceiver).addListener(participantListMessageListenerArgumentCaptor.capture()); + + participantListMessageListener = participantListMessageListenerArgumentCaptor.getValue(); + + expectedJoined = new ArrayList<>(); + expectedUpdated = new ArrayList<>(); + expectedLeft = new ArrayList<>(); + expectedUnchanged = new ArrayList<>(); + } + + @Test + public void testUsersInRoomJoinRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomJoinRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", "theUserId4")); + participants.add(builder.newUser(DISCONNECTED, 5, "theSessionId5", "theUserId5")); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomJoinRoomThenJoinCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomJoinRoomThenJoinCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomJoinRoomAndCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomJoinRoomAndCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomJoinRoomAndCallRepeated() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + participantListMessageListener.onUsersInRoom(participants); + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomChangeCallFlags() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomChangeCallFlagsSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", "theUserId4")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + expectedUpdated.add(builder.newUser(IN_CALL, 1, "theSessionId1", "theUserId1")); + expectedUpdated.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", "theUserId4")); + expectedUnchanged.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomChangeLastPing() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomChangeLastPingSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", "theUserId3")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 108, "theSessionId2")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 815, "theSessionId3", "theUserId3")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomLeaveCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomLeaveCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomLeaveCallThenLeaveRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomLeaveCallThenLeaveRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomLeaveCallAndRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + + participantListMessageListener.onUsersInRoom(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomLeaveCallAndRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated), + argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged)); + } + + @Test + public void testUsersInRoomSeveralEventsSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + participants.add(builder.newUser(IN_CALL, 5, "theSessionId5", "theUserId5")); + // theSessionId6 has not joined yet. + participants.add(builder.newGuest(IN_CALL | WITH_VIDEO, 7, "theSessionId7")); + participants.add(builder.newUser(DISCONNECTED, 8, "theSessionId8", "theUserId8")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 9, "theSessionId9", "theUserId9")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + // theSessionId1 is gone. + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", "theUserId5")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6")); + participants.add(builder.newGuest(IN_CALL, 7, "theSessionId7")); + participants.add(builder.newUser(IN_CALL, 8, "theSessionId8", "theUserId8")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", "theUserId9")); + + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6")); + expectedJoined.add(builder.newUser(IN_CALL, 8, "theSessionId8", "theUserId8")); + expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", "theUserId5")); + expectedUpdated.add(builder.newGuest(IN_CALL, 7, "theSessionId7")); + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + // Last ping is not seen as changed, even if it did. + expectedUnchanged.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", "theUserId9")); + + verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), + eq(expectedUpdated), + argThat(matchesExpectedLeftIgnoringOrder), + eq(expectedUnchanged)); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/CallParticipantListTest.java b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListTest.java new file mode 100644 index 0000000..8d344de --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListTest.java @@ -0,0 +1,48 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call; + +import com.nextcloud.talk.signaling.SignalingMessageReceiver; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class CallParticipantListTest { + + private SignalingMessageReceiver mockedSignalingMessageReceiver; + + private CallParticipantList callParticipantList; + private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener; + + @Before + public void setUp() { + mockedSignalingMessageReceiver = mock(SignalingMessageReceiver.class); + + callParticipantList = new CallParticipantList(mockedSignalingMessageReceiver); + + // Get internal ParticipantListMessageListener from callParticipantList set in the + // mockedSignalingMessageReceiver. + ArgumentCaptor participantListMessageListenerArgumentCaptor = + ArgumentCaptor.forClass(SignalingMessageReceiver.ParticipantListMessageListener.class); + + verify(mockedSignalingMessageReceiver).addListener(participantListMessageListenerArgumentCaptor.capture()); + + participantListMessageListener = participantListMessageListenerArgumentCaptor.getValue(); + } + + @Test + public void testDestroy() { + callParticipantList.destroy(); + + verify(mockedSignalingMessageReceiver).removeListener(participantListMessageListener); + verifyNoMoreInteractions(mockedSignalingMessageReceiver); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/CallParticipantModelTest.kt b/app/src/test/java/com/nextcloud/talk/call/CallParticipantModelTest.kt new file mode 100644 index 0000000..cd088bc --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/CallParticipantModelTest.kt @@ -0,0 +1,52 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito + +class CallParticipantModelTest { + private var callParticipantModel: MutableCallParticipantModel? = null + private var mockedCallParticipantModelObserver: CallParticipantModel.Observer? = null + + @Before + fun setUp() { + callParticipantModel = MutableCallParticipantModel("theSessionId") + mockedCallParticipantModelObserver = Mockito.mock(CallParticipantModel.Observer::class.java) + } + + @Test + fun testSetRaisedHand() { + callParticipantModel!!.addObserver(mockedCallParticipantModelObserver) + callParticipantModel!!.setRaisedHand(true, 4815162342L) + Mockito.verify(mockedCallParticipantModelObserver, Mockito.only())?.onChange() + } + + @Test + fun testSetRaisedHandTwice() { + callParticipantModel!!.addObserver(mockedCallParticipantModelObserver) + callParticipantModel!!.setRaisedHand(true, 4815162342L) + callParticipantModel!!.setRaisedHand(false, 4815162342108L) + Mockito.verify(mockedCallParticipantModelObserver, Mockito.times(2))?.onChange() + } + + @Test + fun testSetRaisedHandTwiceWithSameValue() { + callParticipantModel!!.addObserver(mockedCallParticipantModelObserver) + callParticipantModel!!.setRaisedHand(true, 4815162342L) + callParticipantModel!!.setRaisedHand(true, 4815162342L) + Mockito.verify(mockedCallParticipantModelObserver, Mockito.only())?.onChange() + } + + @Test + fun testEmitReaction() { + callParticipantModel!!.addObserver(mockedCallParticipantModelObserver) + callParticipantModel!!.emitReaction("theReaction") + Mockito.verify(mockedCallParticipantModelObserver, Mockito.only())?.onReaction("theReaction") + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/LocalCallParticipantModelTest.kt b/app/src/test/java/com/nextcloud/talk/call/LocalCallParticipantModelTest.kt new file mode 100644 index 0000000..2440fd0 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/LocalCallParticipantModelTest.kt @@ -0,0 +1,168 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito + +class LocalCallParticipantModelTest { + private var localCallParticipantModel: MutableLocalCallParticipantModel? = null + private var mockedLocalCallParticipantModelObserver: LocalCallParticipantModel.Observer? = null + + @Before + fun setUp() { + localCallParticipantModel = MutableLocalCallParticipantModel() + mockedLocalCallParticipantModelObserver = Mockito.mock(LocalCallParticipantModel.Observer::class.java) + } + + @Test + fun testSetAudioEnabled() { + localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver) + + localCallParticipantModel!!.isAudioEnabled = true + + assertTrue(localCallParticipantModel!!.isAudioEnabled) + assertFalse(localCallParticipantModel!!.isSpeaking) + assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted) + Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange() + } + + @Test + fun testSetAudioEnabledWhileSpeakingWhileMuted() { + localCallParticipantModel!!.isSpeaking = true + + localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver) + + localCallParticipantModel!!.isAudioEnabled = true + + assertTrue(localCallParticipantModel!!.isAudioEnabled) + assertTrue(localCallParticipantModel!!.isSpeaking) + assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted) + Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange() + } + + @Test + fun testSetAudioEnabledTwiceWhileSpeakingWhileMuted() { + localCallParticipantModel!!.isSpeaking = true + + localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver) + + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isAudioEnabled = true + + assertTrue(localCallParticipantModel!!.isAudioEnabled) + assertTrue(localCallParticipantModel!!.isSpeaking) + assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted) + Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange() + } + + @Test + fun testSetAudioDisabled() { + localCallParticipantModel!!.isAudioEnabled = true + + localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver) + + localCallParticipantModel!!.isAudioEnabled = false + + assertFalse(localCallParticipantModel!!.isAudioEnabled) + assertFalse(localCallParticipantModel!!.isSpeaking) + assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted) + Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange() + } + + @Test + fun testSetAudioDisabledWhileSpeaking() { + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = true + + localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver) + + localCallParticipantModel!!.isAudioEnabled = false + + assertFalse(localCallParticipantModel!!.isAudioEnabled) + assertFalse(localCallParticipantModel!!.isSpeaking) + assertTrue(localCallParticipantModel!!.isSpeakingWhileMuted) + Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange() + } + + @Test + fun testSetAudioDisabledTwiceWhileSpeaking() { + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = true + + localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver) + + localCallParticipantModel!!.isAudioEnabled = false + localCallParticipantModel!!.isAudioEnabled = false + + assertFalse(localCallParticipantModel!!.isAudioEnabled) + assertFalse(localCallParticipantModel!!.isSpeaking) + assertTrue(localCallParticipantModel!!.isSpeakingWhileMuted) + Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange() + } + + @Test + fun testSetSpeakingWhileAudioEnabled() { + localCallParticipantModel!!.isAudioEnabled = true + + localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver) + + localCallParticipantModel!!.isSpeaking = true + + assertTrue(localCallParticipantModel!!.isAudioEnabled) + assertTrue(localCallParticipantModel!!.isSpeaking) + assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted) + Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange() + } + + @Test + fun testSetNotSpeakingWhileAudioEnabled() { + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = true + + localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver) + + localCallParticipantModel!!.isSpeaking = false + + assertTrue(localCallParticipantModel!!.isAudioEnabled) + assertFalse(localCallParticipantModel!!.isSpeaking) + assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted) + Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange() + } + + @Test + fun testSetSpeakingWhileAudioDisabled() { + localCallParticipantModel!!.isAudioEnabled = false + + localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver) + + localCallParticipantModel!!.isSpeaking = true + + assertFalse(localCallParticipantModel!!.isAudioEnabled) + assertFalse(localCallParticipantModel!!.isSpeaking) + assertTrue(localCallParticipantModel!!.isSpeakingWhileMuted) + Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange() + } + + @Test + fun testSetNotSpeakingWhileAudioDisabled() { + localCallParticipantModel!!.isAudioEnabled = false + localCallParticipantModel!!.isSpeaking = true + + localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver) + + localCallParticipantModel!!.isSpeaking = false + + assertFalse(localCallParticipantModel!!.isAudioEnabled) + assertFalse(localCallParticipantModel!!.isSpeaking) + assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted) + Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange() + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterMcuTest.kt b/app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterMcuTest.kt new file mode 100644 index 0000000..03e3245 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterMcuTest.kt @@ -0,0 +1,641 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.models.json.signaling.NCMessagePayload +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.TestScheduler +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.times +import java.util.concurrent.TimeUnit + +@Suppress("LongMethod") +class LocalStateBroadcasterMcuTest { + + private var localCallParticipantModel: MutableLocalCallParticipantModel? = null + private var mockedMessageSender: MessageSender? = null + + private var localStateBroadcasterMcu: LocalStateBroadcasterMcu? = null + + @Before + fun setUp() { + localCallParticipantModel = MutableLocalCallParticipantModel() + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = true + localCallParticipantModel!!.isVideoEnabled = true + mockedMessageSender = Mockito.mock(MessageSender::class.java) + } + + private fun getExpectedUnmuteAudio(): NCSignalingMessage { + val expectedUnmuteAudio = NCSignalingMessage() + expectedUnmuteAudio.roomType = "video" + expectedUnmuteAudio.type = "unmute" + + val payload = NCMessagePayload() + payload.name = "audio" + expectedUnmuteAudio.payload = payload + + return expectedUnmuteAudio + } + + private fun getExpectedMuteAudio(): NCSignalingMessage { + val expectedMuteAudio = NCSignalingMessage() + expectedMuteAudio.roomType = "video" + expectedMuteAudio.type = "mute" + + val payload = NCMessagePayload() + payload.name = "audio" + expectedMuteAudio.payload = payload + + return expectedMuteAudio + } + + private fun getExpectedUnmuteVideo(): NCSignalingMessage { + val expectedUnmuteVideo = NCSignalingMessage() + expectedUnmuteVideo.roomType = "video" + expectedUnmuteVideo.type = "unmute" + + val payload = NCMessagePayload() + payload.name = "video" + expectedUnmuteVideo.payload = payload + + return expectedUnmuteVideo + } + + private fun getExpectedMuteVideo(): NCSignalingMessage { + val expectedMuteVideo = NCSignalingMessage() + expectedMuteVideo.roomType = "video" + expectedMuteVideo.type = "mute" + + val payload = NCMessagePayload() + payload.name = "video" + expectedMuteVideo.payload = payload + + return expectedMuteVideo + } + + @Test + fun testStateSentWithExponentialBackoffWhenParticipantAdded() { + val testScheduler = TestScheduler() + RxJavaPlugins.setIoSchedulerHandler { testScheduler } + + localStateBroadcasterMcu = LocalStateBroadcasterMcu( + localCallParticipantModel, + mockedMessageSender + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel) + + // Sending will be done in another thread, so just adding the participant does not send anything until that + // other thread could run. + Mockito.verifyNoInteractions(mockedMessageSender) + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteAudio = getExpectedUnmuteAudio() + val expectedUnmuteVideo = getExpectedUnmuteVideo() + + testScheduler.advanceTimeBy(0, TimeUnit.SECONDS) + + var messageCount = 1 + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS) + + messageCount = 2 + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + + messageCount = 3 + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS) + + messageCount = 4 + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(8, TimeUnit.SECONDS) + + messageCount = 5 + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(16, TimeUnit.SECONDS) + + messageCount = 6 + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testStateSentWithExponentialBackoffIsTheCurrentState() { + // This test could have been included in "testStateSentWithExponentialBackoffWhenParticipantAdded", but was + // kept separate for clarity. + + val testScheduler = TestScheduler() + RxJavaPlugins.setIoSchedulerHandler { testScheduler } + + localStateBroadcasterMcu = LocalStateBroadcasterMcu( + localCallParticipantModel, + mockedMessageSender + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel) + + // Sending will be done in another thread, so just adding the participant does not send anything until that + // other thread could run. + Mockito.verifyNoInteractions(mockedMessageSender) + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteAudio = getExpectedUnmuteAudio() + val expectedUnmuteVideo = getExpectedUnmuteVideo() + + testScheduler.advanceTimeBy(0, TimeUnit.SECONDS) + + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + localCallParticipantModel!!.isSpeaking = false + + val expectedStoppedSpeaking = DataChannelMessage("stoppedSpeaking") + + // Changing the state causes the normal state update to be sent, independently of the initial state + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedStoppedSpeaking) + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS) + + Mockito.verify(mockedMessageSender!!, times(2)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(2)).sendToAll(expectedStoppedSpeaking) + Mockito.verify(mockedMessageSender!!, times(2)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(2)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(2)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + localCallParticipantModel!!.isAudioEnabled = false + + val expectedAudioOff = DataChannelMessage("audioOff") + val expectedMuteAudio = getExpectedMuteAudio() + + // Changing the state causes the normal state update to be sent, independently of the initial state + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedAudioOff) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteAudio) + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + + Mockito.verify(mockedMessageSender!!, times(2)).sendToAll(expectedAudioOff) + Mockito.verify(mockedMessageSender!!, times(3)).sendToAll(expectedStoppedSpeaking) + Mockito.verify(mockedMessageSender!!, times(3)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteAudio) + Mockito.verify(mockedMessageSender!!, times(1)).send(expectedMuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(3)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + localCallParticipantModel!!.isVideoEnabled = false + + val expectedVideoOff = DataChannelMessage("videoOff") + val expectedMuteVideo = getExpectedMuteVideo() + + // Changing the state causes the normal state update to be sent, independently of the initial state + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedVideoOff) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteVideo) + + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS) + + Mockito.verify(mockedMessageSender!!, times(3)).sendToAll(expectedAudioOff) + Mockito.verify(mockedMessageSender!!, times(4)).sendToAll(expectedStoppedSpeaking) + Mockito.verify(mockedMessageSender!!, times(2)).sendToAll(expectedVideoOff) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteAudio) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteVideo) + Mockito.verify(mockedMessageSender!!, times(2)).send(expectedMuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(1)).send(expectedMuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + localCallParticipantModel!!.isVideoEnabled = true + + // Changing the state causes the normal state update to be sent, independently of the initial state + Mockito.verify(mockedMessageSender!!, times(4)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedUnmuteVideo) + + testScheduler.advanceTimeBy(8, TimeUnit.SECONDS) + + Mockito.verify(mockedMessageSender!!, times(4)).sendToAll(expectedAudioOff) + Mockito.verify(mockedMessageSender!!, times(5)).sendToAll(expectedStoppedSpeaking) + Mockito.verify(mockedMessageSender!!, times(5)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteAudio) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedMuteVideo) + Mockito.verify(mockedMessageSender!!, times(1)).sendToAll(expectedUnmuteVideo) + Mockito.verify(mockedMessageSender!!, times(3)).send(expectedMuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(4)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testStateSentWithExponentialBackoffWhenAnotherParticipantAdded() { + // The state sent through data channels should be restarted, although the state sent through signaling + // messages should be independent for each participant. + + val testScheduler = TestScheduler() + RxJavaPlugins.setIoSchedulerHandler { testScheduler } + + localStateBroadcasterMcu = LocalStateBroadcasterMcu( + localCallParticipantModel, + mockedMessageSender + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel) + + // Sending will be done in another thread, so just adding the participant does not send anything until that + // other thread could run. + Mockito.verifyNoInteractions(mockedMessageSender) + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteAudio = getExpectedUnmuteAudio() + val expectedUnmuteVideo = getExpectedUnmuteVideo() + + testScheduler.advanceTimeBy(0, TimeUnit.SECONDS) + + var dataChannelMessageCount = 1 + var signalingMessageCount1 = 1 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS) + + dataChannelMessageCount = 2 + signalingMessageCount1 = 2 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + + dataChannelMessageCount = 3 + signalingMessageCount1 = 3 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS) + + dataChannelMessageCount = 4 + signalingMessageCount1 = 4 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + val callParticipantModel2 = MutableCallParticipantModel("theSessionId2") + + localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel2) + + testScheduler.advanceTimeBy(0, TimeUnit.SECONDS) + + dataChannelMessageCount = 5 + var signalingMessageCount2 = 1 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS) + + dataChannelMessageCount = 6 + signalingMessageCount2 = 2 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + + dataChannelMessageCount = 7 + signalingMessageCount2 = 3 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS) + + dataChannelMessageCount = 8 + signalingMessageCount2 = 4 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + // 0+1+2+4+1=8 seconds since last signaling messages for participant 1 + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS) + + signalingMessageCount1 = 5 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + // 1+7=8 seconds since last data channel messages and signaling messages for participant 2 + testScheduler.advanceTimeBy(7, TimeUnit.SECONDS) + + dataChannelMessageCount = 9 + signalingMessageCount2 = 5 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + // 7+9=16 seconds since last signaling messages for participant 1 + testScheduler.advanceTimeBy(9, TimeUnit.SECONDS) + + signalingMessageCount1 = 6 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + // 9+7=16 seconds since last data channel messages and signaling messages for participant 2 + testScheduler.advanceTimeBy(7, TimeUnit.SECONDS) + + dataChannelMessageCount = 10 + signalingMessageCount2 = 6 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount1)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount2)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testStateSentWithExponentialBackoffWhenParticipantRemoved() { + // For simplicity the exponential backoff is not aborted when the participant that triggered it is removed. + // However, the signaling messages are stopped when the participant is removed. + + val testScheduler = TestScheduler() + RxJavaPlugins.setIoSchedulerHandler { testScheduler } + + localStateBroadcasterMcu = LocalStateBroadcasterMcu( + localCallParticipantModel, + mockedMessageSender + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel) + + // Sending will be done in another thread, so just adding the participant does not send anything until that + // other thread could run. + Mockito.verifyNoInteractions(mockedMessageSender) + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteAudio = getExpectedUnmuteAudio() + val expectedUnmuteVideo = getExpectedUnmuteVideo() + + testScheduler.advanceTimeBy(0, TimeUnit.SECONDS) + + var dataChannelMessageCount = 1 + var signalingMessageCount = 1 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS) + + dataChannelMessageCount = 2 + signalingMessageCount = 2 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + + dataChannelMessageCount = 3 + signalingMessageCount = 3 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS) + + dataChannelMessageCount = 4 + signalingMessageCount = 4 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + localStateBroadcasterMcu!!.handleCallParticipantRemoved(callParticipantModel) + + testScheduler.advanceTimeBy(8, TimeUnit.SECONDS) + + dataChannelMessageCount = 5 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(16, TimeUnit.SECONDS) + + dataChannelMessageCount = 6 + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(dataChannelMessageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(signalingMessageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testStateNoLongerSentOnceDestroyed() { + val testScheduler = TestScheduler() + RxJavaPlugins.setIoSchedulerHandler { testScheduler } + + localStateBroadcasterMcu = LocalStateBroadcasterMcu( + localCallParticipantModel, + mockedMessageSender + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + val callParticipantModel2 = MutableCallParticipantModel("theSessionId2") + + localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel) + localStateBroadcasterMcu!!.handleCallParticipantAdded(callParticipantModel2) + + // Sending will be done in another thread, so just adding the participant does not send anything until that + // other thread could run. + Mockito.verifyNoInteractions(mockedMessageSender) + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteAudio = getExpectedUnmuteAudio() + val expectedUnmuteVideo = getExpectedUnmuteVideo() + + testScheduler.advanceTimeBy(0, TimeUnit.SECONDS) + + var messageCount = 1 + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS) + + messageCount = 2 + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + + messageCount = 3 + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!, times(messageCount)).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSender!!, times(messageCount)).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSender) + + localStateBroadcasterMcu!!.destroy() + + testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterNoMcuTest.kt b/app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterNoMcuTest.kt new file mode 100644 index 0000000..f225ff7 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterNoMcuTest.kt @@ -0,0 +1,357 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.models.json.signaling.NCMessagePayload +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.webrtc.PeerConnection + +class LocalStateBroadcasterNoMcuTest { + + private var localCallParticipantModel: MutableLocalCallParticipantModel? = null + private var mockedMessageSenderNoMcu: MessageSenderNoMcu? = null + + private var localStateBroadcasterNoMcu: LocalStateBroadcasterNoMcu? = null + + @Before + fun setUp() { + localCallParticipantModel = MutableLocalCallParticipantModel() + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = true + localCallParticipantModel!!.isVideoEnabled = true + mockedMessageSenderNoMcu = Mockito.mock(MessageSenderNoMcu::class.java) + } + + private fun getExpectedUnmuteAudio(): NCSignalingMessage { + val expectedUnmuteAudio = NCSignalingMessage() + expectedUnmuteAudio.roomType = "video" + expectedUnmuteAudio.type = "unmute" + + val payload = NCMessagePayload() + payload.name = "audio" + expectedUnmuteAudio.payload = payload + + return expectedUnmuteAudio + } + + private fun getExpectedUnmuteVideo(): NCSignalingMessage { + val expectedUnmuteVideo = NCSignalingMessage() + expectedUnmuteVideo.roomType = "video" + expectedUnmuteVideo.type = "unmute" + + val payload = NCMessagePayload() + payload.name = "video" + expectedUnmuteVideo.payload = payload + + return expectedUnmuteVideo + } + + @Test + fun testStateSentWhenIceConnected() { + localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu( + localCallParticipantModel, + mockedMessageSenderNoMcu + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteAudio = getExpectedUnmuteAudio() + val expectedUnmuteVideo = getExpectedUnmuteVideo() + + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + } + + @Test + fun testStateSentWhenIceCompleted() { + localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu( + localCallParticipantModel, + mockedMessageSenderNoMcu + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.COMPLETED) + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteAudio = getExpectedUnmuteAudio() + val expectedUnmuteVideo = getExpectedUnmuteVideo() + + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + } + + @Test + fun testStateNotSentWhenIceCompletedAfterConnected() { + localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu( + localCallParticipantModel, + mockedMessageSenderNoMcu + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteAudio = getExpectedUnmuteAudio() + val expectedUnmuteVideo = getExpectedUnmuteVideo() + + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.COMPLETED) + + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + } + + @Test + fun testStateNotSentWhenIceConnectedAgain() { + localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu( + localCallParticipantModel, + mockedMessageSenderNoMcu + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteAudio = getExpectedUnmuteAudio() + val expectedUnmuteVideo = getExpectedUnmuteVideo() + + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.COMPLETED) + + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + + // Completed -> Connected could happen with an ICE restart + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.DISCONNECTED) + + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + + // Failed -> Checking could happen with an ICE restart + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.FAILED) + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + } + + @Test + fun testStateNotSentToOtherParticipantsWhenIceConnected() { + localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu( + localCallParticipantModel, + mockedMessageSenderNoMcu + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + val callParticipantModel2 = MutableCallParticipantModel("theSessionId2") + + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel) + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel2) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + callParticipantModel2.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteAudio = getExpectedUnmuteAudio() + val expectedUnmuteVideo = getExpectedUnmuteVideo() + + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId") + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + + callParticipantModel2.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedAudioOn, "theSessionId2") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedSpeaking, "theSessionId2") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedVideoOn, "theSessionId2") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteAudio, "theSessionId2") + Mockito.verify(mockedMessageSenderNoMcu!!).send(expectedUnmuteVideo, "theSessionId2") + Mockito.verifyNoMoreInteractions(mockedMessageSenderNoMcu) + } + + @Test + fun testStateNotSentWhenIceConnectedAfterParticipantIsRemoved() { + // This should not happen, as peer connections are expected to be ended when a call participant is removed, but + // just in case. + + localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu( + localCallParticipantModel, + mockedMessageSenderNoMcu + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + + localStateBroadcasterNoMcu!!.handleCallParticipantRemoved(callParticipantModel) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + } + + @Test + fun testStateNotSentWhenIceCompletedAfterParticipantIsRemoved() { + // This should not happen, as peer connections are expected to be ended when a call participant is removed, but + // just in case. + + localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu( + localCallParticipantModel, + mockedMessageSenderNoMcu + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + + localStateBroadcasterNoMcu!!.handleCallParticipantRemoved(callParticipantModel) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.COMPLETED) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + } + + @Test + fun testStateNotSentWhenIceConnectedAfterDestroyed() { + localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu( + localCallParticipantModel, + mockedMessageSenderNoMcu + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + val callParticipantModel2 = MutableCallParticipantModel("theSessionId2") + + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel) + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel2) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + callParticipantModel2.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + + localStateBroadcasterNoMcu!!.destroy() + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + callParticipantModel2.setIceConnectionState(PeerConnection.IceConnectionState.CONNECTED) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + } + + @Test + fun testStateNotSentWhenIceCompletedAfterDestroyed() { + localStateBroadcasterNoMcu = LocalStateBroadcasterNoMcu( + localCallParticipantModel, + mockedMessageSenderNoMcu + ) + + val callParticipantModel = MutableCallParticipantModel("theSessionId") + + localStateBroadcasterNoMcu!!.handleCallParticipantAdded(callParticipantModel) + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.CHECKING) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + + localStateBroadcasterNoMcu!!.destroy() + + callParticipantModel.setIceConnectionState(PeerConnection.IceConnectionState.COMPLETED) + + Mockito.verifyNoInteractions(mockedMessageSenderNoMcu) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterTest.kt b/app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterTest.kt new file mode 100644 index 0000000..34ca59e --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/LocalStateBroadcasterTest.kt @@ -0,0 +1,324 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.models.json.signaling.NCMessagePayload +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito + +@Suppress("TooManyFunctions") +class LocalStateBroadcasterTest { + + private class LocalStateBroadcaster( + localCallParticipantModel: LocalCallParticipantModel?, + messageSender: MessageSender? + ) : com.nextcloud.talk.call.LocalStateBroadcaster(localCallParticipantModel, messageSender) { + + override fun handleCallParticipantAdded(callParticipantModel: CallParticipantModel) { + // Not used in base class tests + } + + override fun handleCallParticipantRemoved(callParticipantModel: CallParticipantModel) { + // Not used in base class tests + } + } + + private var localCallParticipantModel: MutableLocalCallParticipantModel? = null + private var mockedMessageSender: MessageSender? = null + + private var localStateBroadcaster: LocalStateBroadcaster? = null + + @Before + fun setUp() { + localCallParticipantModel = MutableLocalCallParticipantModel() + mockedMessageSender = Mockito.mock(MessageSender::class.java) + } + + @Test + fun testEnableAudio() { + localCallParticipantModel!!.isAudioEnabled = false + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isAudioEnabled = true + + val expectedAudioOn = DataChannelMessage("audioOn") + + val expectedUnmuteAudio = NCSignalingMessage() + expectedUnmuteAudio.roomType = "video" + expectedUnmuteAudio.type = "unmute" + val payload = NCMessagePayload() + payload.name = "audio" + expectedUnmuteAudio.payload = payload + + Mockito.verify(mockedMessageSender!!).sendToAll(expectedAudioOn) + Mockito.verify(mockedMessageSender!!).sendToAll(expectedUnmuteAudio) + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testEnableAudioTwice() { + localCallParticipantModel!!.isAudioEnabled = true + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isAudioEnabled = true + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testDisableAudio() { + localCallParticipantModel!!.isAudioEnabled = true + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isAudioEnabled = false + + val expectedAudioOff = DataChannelMessage("audioOff") + + val expectedMuteAudio = NCSignalingMessage() + expectedMuteAudio.roomType = "video" + expectedMuteAudio.type = "mute" + val payload = NCMessagePayload() + payload.name = "audio" + expectedMuteAudio.payload = payload + + Mockito.verify(mockedMessageSender!!).sendToAll(expectedAudioOff) + Mockito.verify(mockedMessageSender!!).sendToAll(expectedMuteAudio) + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testDisableAudioTwice() { + localCallParticipantModel!!.isAudioEnabled = false + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isAudioEnabled = false + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testEnableSpeaking() { + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = false + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isSpeaking = true + + val expectedSpeaking = DataChannelMessage("speaking") + + Mockito.verify(mockedMessageSender!!).sendToAll(expectedSpeaking) + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testEnableSpeakingTwice() { + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = true + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isSpeaking = true + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testEnableSpeakingWithAudioDisabled() { + localCallParticipantModel!!.isAudioEnabled = false + localCallParticipantModel!!.isSpeaking = false + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isSpeaking = true + + Mockito.verifyNoInteractions(mockedMessageSender) + } + + @Test + fun testEnableAudioWhileSpeaking() { + localCallParticipantModel!!.isAudioEnabled = false + localCallParticipantModel!!.isSpeaking = false + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isSpeaking = true + localCallParticipantModel!!.isAudioEnabled = true + + val expectedAudioOn = DataChannelMessage("audioOn") + val expectedSpeaking = DataChannelMessage("speaking") + + val expectedUnmuteAudio = NCSignalingMessage() + expectedUnmuteAudio.roomType = "video" + expectedUnmuteAudio.type = "unmute" + val payload = NCMessagePayload() + payload.name = "audio" + expectedUnmuteAudio.payload = payload + + val inOrder = Mockito.inOrder(mockedMessageSender) + + inOrder.verify(mockedMessageSender!!).sendToAll(expectedAudioOn) + inOrder.verify(mockedMessageSender!!).sendToAll(expectedSpeaking) + Mockito.verify(mockedMessageSender!!).sendToAll(expectedUnmuteAudio) + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testDisableSpeaking() { + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = true + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isSpeaking = false + + val expectedStoppedSpeaking = DataChannelMessage("stoppedSpeaking") + + Mockito.verify(mockedMessageSender!!).sendToAll(expectedStoppedSpeaking) + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testDisableSpeakingTwice() { + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = false + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isSpeaking = false + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testDisableAudioWhileSpeaking() { + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = true + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isAudioEnabled = false + + val expectedStoppedSpeaking = DataChannelMessage("stoppedSpeaking") + val expectedAudioOff = DataChannelMessage("audioOff") + + val expectedMuteAudio = NCSignalingMessage() + expectedMuteAudio.roomType = "video" + expectedMuteAudio.type = "mute" + val payload = NCMessagePayload() + payload.name = "audio" + expectedMuteAudio.payload = payload + + val inOrder = Mockito.inOrder(mockedMessageSender) + + inOrder.verify(mockedMessageSender!!).sendToAll(expectedStoppedSpeaking) + inOrder.verify(mockedMessageSender!!).sendToAll(expectedAudioOff) + Mockito.verify(mockedMessageSender!!).sendToAll(expectedMuteAudio) + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testDisableSpeakingWithAudioDisabled() { + localCallParticipantModel!!.isAudioEnabled = false + localCallParticipantModel!!.isSpeaking = true + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isSpeaking = false + + Mockito.verifyNoInteractions(mockedMessageSender) + } + + @Test + fun testEnableVideo() { + localCallParticipantModel!!.isVideoEnabled = false + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isVideoEnabled = true + + val expectedVideoOn = DataChannelMessage("videoOn") + + val expectedUnmuteVideo = NCSignalingMessage() + expectedUnmuteVideo.roomType = "video" + expectedUnmuteVideo.type = "unmute" + val payload = NCMessagePayload() + payload.name = "video" + expectedUnmuteVideo.payload = payload + + Mockito.verify(mockedMessageSender!!).sendToAll(expectedVideoOn) + Mockito.verify(mockedMessageSender!!).sendToAll(expectedUnmuteVideo) + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testEnableVideoTwice() { + localCallParticipantModel!!.isVideoEnabled = true + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isVideoEnabled = true + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testDisableVideo() { + localCallParticipantModel!!.isVideoEnabled = true + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isVideoEnabled = false + + val expectedVideoOff = DataChannelMessage("videoOff") + + val expectedMuteVideo = NCSignalingMessage() + expectedMuteVideo.roomType = "video" + expectedMuteVideo.type = "mute" + val payload = NCMessagePayload() + payload.name = "video" + expectedMuteVideo.payload = payload + + Mockito.verify(mockedMessageSender!!).sendToAll(expectedVideoOff) + Mockito.verify(mockedMessageSender!!).sendToAll(expectedMuteVideo) + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testDisableVideoTwice() { + localCallParticipantModel!!.isVideoEnabled = false + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localCallParticipantModel!!.isVideoEnabled = false + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } + + @Test + fun testChangeStateAfterDestroying() { + localCallParticipantModel!!.isAudioEnabled = false + localCallParticipantModel!!.isSpeaking = false + localCallParticipantModel!!.isVideoEnabled = false + + localStateBroadcaster = LocalStateBroadcaster(localCallParticipantModel, mockedMessageSender) + + localStateBroadcaster!!.destroy() + localCallParticipantModel!!.isAudioEnabled = true + localCallParticipantModel!!.isSpeaking = true + localCallParticipantModel!!.isVideoEnabled = true + + Mockito.verifyNoMoreInteractions(mockedMessageSender) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt b/app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt new file mode 100644 index 0000000..9fd8d62 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt @@ -0,0 +1,118 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.signaling.SignalingMessageSender +import com.nextcloud.talk.webrtc.PeerConnectionWrapper +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.never + +class MessageSenderMcuTest { + + private var peerConnectionWrappers: MutableList? = null + private var peerConnectionWrapper1: PeerConnectionWrapper? = null + private var peerConnectionWrapper2: PeerConnectionWrapper? = null + private var peerConnectionWrapper2Screen: PeerConnectionWrapper? = null + private var peerConnectionWrapper4Screen: PeerConnectionWrapper? = null + private var ownPeerConnectionWrapper: PeerConnectionWrapper? = null + private var ownPeerConnectionWrapperScreen: PeerConnectionWrapper? = null + + private var messageSender: MessageSenderMcu? = null + + @Before + fun setUp() { + val signalingMessageSender = Mockito.mock(SignalingMessageSender::class.java) + + val callParticipants = HashMap() + + peerConnectionWrappers = ArrayList() + + peerConnectionWrapper1 = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper1!!.sessionId).thenReturn("theSessionId1") + Mockito.`when`(peerConnectionWrapper1!!.videoStreamType).thenReturn("video") + peerConnectionWrappers!!.add(peerConnectionWrapper1) + + peerConnectionWrapper2 = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper2!!.sessionId).thenReturn("theSessionId2") + Mockito.`when`(peerConnectionWrapper2!!.videoStreamType).thenReturn("video") + peerConnectionWrappers!!.add(peerConnectionWrapper2) + + peerConnectionWrapper2Screen = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper2Screen!!.sessionId).thenReturn("theSessionId2") + Mockito.`when`(peerConnectionWrapper2Screen!!.videoStreamType).thenReturn("screen") + peerConnectionWrappers!!.add(peerConnectionWrapper2Screen) + + peerConnectionWrapper4Screen = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper4Screen!!.sessionId).thenReturn("theSessionId4") + Mockito.`when`(peerConnectionWrapper4Screen!!.videoStreamType).thenReturn("screen") + peerConnectionWrappers!!.add(peerConnectionWrapper4Screen) + + ownPeerConnectionWrapper = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(ownPeerConnectionWrapper!!.sessionId).thenReturn("ownSessionId") + Mockito.`when`(ownPeerConnectionWrapper!!.videoStreamType).thenReturn("video") + peerConnectionWrappers!!.add(ownPeerConnectionWrapper) + + ownPeerConnectionWrapperScreen = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(ownPeerConnectionWrapperScreen!!.sessionId).thenReturn("ownSessionId") + Mockito.`when`(ownPeerConnectionWrapperScreen!!.videoStreamType).thenReturn("screen") + peerConnectionWrappers!!.add(ownPeerConnectionWrapperScreen) + + messageSender = MessageSenderMcu( + signalingMessageSender, + callParticipants.keys, + peerConnectionWrappers, + "ownSessionId" + ) + } + + @Test + fun testSendDataChannelMessageToAll() { + val message = DataChannelMessage() + messageSender!!.sendToAll(message) + + Mockito.verify(ownPeerConnectionWrapper!!).send(message) + Mockito.verify(ownPeerConnectionWrapperScreen!!, never()).send(message) + Mockito.verify(peerConnectionWrapper1!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message) + Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message) + } + + @Test + fun testSendDataChannelMessageToAllIfOwnScreenPeerConnection() { + peerConnectionWrappers!!.remove(ownPeerConnectionWrapper) + + val message = DataChannelMessage() + messageSender!!.sendToAll(message) + + Mockito.verify(ownPeerConnectionWrapper!!, never()).send(message) + Mockito.verify(ownPeerConnectionWrapperScreen!!, never()).send(message) + Mockito.verify(peerConnectionWrapper1!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message) + Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message) + } + + @Test + fun testSendDataChannelMessageToAllWithoutOwnPeerConnection() { + peerConnectionWrappers!!.remove(ownPeerConnectionWrapper) + peerConnectionWrappers!!.remove(ownPeerConnectionWrapperScreen) + + val message = DataChannelMessage() + messageSender!!.sendToAll(message) + + Mockito.verify(ownPeerConnectionWrapper!!, never()).send(message) + Mockito.verify(ownPeerConnectionWrapperScreen!!, never()).send(message) + Mockito.verify(peerConnectionWrapper1!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message) + Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt b/app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt new file mode 100644 index 0000000..303108e --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt @@ -0,0 +1,101 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.signaling.SignalingMessageSender +import com.nextcloud.talk.webrtc.PeerConnectionWrapper +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.never + +class MessageSenderNoMcuTest { + + private var peerConnectionWrappers: MutableList? = null + private var peerConnectionWrapper1: PeerConnectionWrapper? = null + private var peerConnectionWrapper2: PeerConnectionWrapper? = null + private var peerConnectionWrapper2Screen: PeerConnectionWrapper? = null + private var peerConnectionWrapper4Screen: PeerConnectionWrapper? = null + + private var messageSender: MessageSenderNoMcu? = null + + @Before + fun setUp() { + val signalingMessageSender = Mockito.mock(SignalingMessageSender::class.java) + + val callParticipants = HashMap() + + peerConnectionWrappers = ArrayList() + + peerConnectionWrapper1 = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper1!!.sessionId).thenReturn("theSessionId1") + Mockito.`when`(peerConnectionWrapper1!!.videoStreamType).thenReturn("video") + peerConnectionWrappers!!.add(peerConnectionWrapper1) + + peerConnectionWrapper2 = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper2!!.sessionId).thenReturn("theSessionId2") + Mockito.`when`(peerConnectionWrapper2!!.videoStreamType).thenReturn("video") + peerConnectionWrappers!!.add(peerConnectionWrapper2) + + peerConnectionWrapper2Screen = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper2Screen!!.sessionId).thenReturn("theSessionId2") + Mockito.`when`(peerConnectionWrapper2Screen!!.videoStreamType).thenReturn("screen") + peerConnectionWrappers!!.add(peerConnectionWrapper2Screen) + + peerConnectionWrapper4Screen = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper4Screen!!.sessionId).thenReturn("theSessionId4") + Mockito.`when`(peerConnectionWrapper4Screen!!.videoStreamType).thenReturn("screen") + peerConnectionWrappers!!.add(peerConnectionWrapper4Screen) + + messageSender = MessageSenderNoMcu(signalingMessageSender, callParticipants.keys, peerConnectionWrappers) + } + + @Test + fun testSendDataChannelMessage() { + val message = DataChannelMessage() + messageSender!!.send(message, "theSessionId2") + + Mockito.verify(peerConnectionWrapper2!!).send(message) + Mockito.verify(peerConnectionWrapper1!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message) + Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message) + } + + @Test + fun testSendDataChannelMessageIfScreenPeerConnection() { + val message = DataChannelMessage() + messageSender!!.send(message, "theSessionId4") + + Mockito.verify(peerConnectionWrapper1!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message) + Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message) + } + + @Test + fun testSendDataChannelMessageIfNoPeerConnection() { + val message = DataChannelMessage() + messageSender!!.send(message, "theSessionId3") + + Mockito.verify(peerConnectionWrapper1!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message) + Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message) + } + + @Test + fun testSendDataChannelMessageToAll() { + val message = DataChannelMessage() + messageSender!!.sendToAll(message) + + Mockito.verify(peerConnectionWrapper1!!).send(message) + Mockito.verify(peerConnectionWrapper2!!).send(message) + Mockito.verify(peerConnectionWrapper2Screen!!, never()).send(message) + Mockito.verify(peerConnectionWrapper4Screen!!, never()).send(message) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/MessageSenderTest.kt b/app/src/test/java/com/nextcloud/talk/call/MessageSenderTest.kt new file mode 100644 index 0000000..46915ef --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/MessageSenderTest.kt @@ -0,0 +1,134 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage +import com.nextcloud.talk.signaling.SignalingMessageSender +import com.nextcloud.talk.webrtc.PeerConnectionWrapper +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.any +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.times +import org.mockito.invocation.InvocationOnMock + +class MessageSenderTest { + + private class MessageSender( + signalingMessageSender: SignalingMessageSender?, + callParticipantSessionIds: Set?, + peerConnectionWrappers: List? + ) : com.nextcloud.talk.call.MessageSender( + signalingMessageSender, + callParticipantSessionIds, + peerConnectionWrappers + ) { + + override fun sendToAll(dataChannelMessage: DataChannelMessage?) { + // Not used in base class tests + } + } + + private var signalingMessageSender: SignalingMessageSender? = null + + private var callParticipants: MutableMap? = null + + private var messageSender: MessageSender? = null + + @Before + fun setUp() { + signalingMessageSender = Mockito.mock(SignalingMessageSender::class.java) + + callParticipants = HashMap() + + val callParticipant1: CallParticipant = Mockito.mock(CallParticipant::class.java) + callParticipants!!["theSessionId1"] = callParticipant1 + + val callParticipant2: CallParticipant = Mockito.mock(CallParticipant::class.java) + callParticipants!!["theSessionId2"] = callParticipant2 + + val callParticipant3: CallParticipant = Mockito.mock(CallParticipant::class.java) + callParticipants!!["theSessionId3"] = callParticipant3 + + val callParticipant4: CallParticipant = Mockito.mock(CallParticipant::class.java) + callParticipants!!["theSessionId4"] = callParticipant4 + + val peerConnectionWrappers = ArrayList() + + messageSender = MessageSender(signalingMessageSender, callParticipants!!.keys, peerConnectionWrappers) + } + + @Test + fun testSendSignalingMessage() { + val message: NCSignalingMessage = Mockito.mock(NCSignalingMessage::class.java) + messageSender!!.send(message, "theSessionId2") + + Mockito.verify(message).to = "theSessionId2" + Mockito.verify(signalingMessageSender!!).send(message) + } + + @Test + fun testSendSignalingMessageIfUnknownSessionId() { + val message: NCSignalingMessage = Mockito.mock(NCSignalingMessage::class.java) + messageSender!!.send(message, "unknownSessionId") + + Mockito.verify(message).to = "unknownSessionId" + Mockito.verify(signalingMessageSender!!).send(message) + } + + @Test + fun testSendSignalingMessageToAll() { + val sentTo: MutableList = ArrayList() + doAnswer { invocation: InvocationOnMock -> + val arguments = invocation.arguments + val message = (arguments[0] as NCSignalingMessage) + + sentTo.add(message.to) + null + }.`when`(signalingMessageSender!!).send(any()) + + val message = NCSignalingMessage() + messageSender!!.sendToAll(message) + + assertTrue(sentTo.contains("theSessionId1")) + assertTrue(sentTo.contains("theSessionId2")) + assertTrue(sentTo.contains("theSessionId3")) + assertTrue(sentTo.contains("theSessionId4")) + Mockito.verify(signalingMessageSender!!, times(4)).send(message) + Mockito.verifyNoMoreInteractions(signalingMessageSender) + } + + @Test + fun testSendSignalingMessageToAllWhenParticipantsWereUpdated() { + val callParticipant5: CallParticipant = Mockito.mock(CallParticipant::class.java) + callParticipants!!["theSessionId5"] = callParticipant5 + + callParticipants!!.remove("theSessionId2") + callParticipants!!.remove("theSessionId3") + + val sentTo: MutableList = ArrayList() + doAnswer { invocation: InvocationOnMock -> + val arguments = invocation.arguments + val message = (arguments[0] as NCSignalingMessage) + + sentTo.add(message.to) + null + }.`when`(signalingMessageSender!!).send(any()) + + val message = NCSignalingMessage() + messageSender!!.sendToAll(message) + + assertTrue(sentTo.contains("theSessionId1")) + assertTrue(sentTo.contains("theSessionId4")) + assertTrue(sentTo.contains("theSessionId5")) + Mockito.verify(signalingMessageSender!!, times(3)).send(message) + Mockito.verifyNoMoreInteractions(signalingMessageSender) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt b/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt new file mode 100644 index 0000000..8a9a569 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/contacts/ContactsViewModelTest.kt @@ -0,0 +1,126 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts + +import com.nextcloud.talk.contacts.apiService.FakeItem +import com.nextcloud.talk.contacts.repository.FakeRepositoryError +import com.nextcloud.talk.contacts.repository.FakeRepositorySuccess +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class ContactsViewModelTest { + private lateinit var viewModel: ContactsViewModel + private val repository: ContactsRepository = FakeRepositorySuccess() + + val dispatcher: TestDispatcher = UnconfinedTestDispatcher() + + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Before + fun setUp() { + viewModel = ContactsViewModel(repository) + } + + @Test + fun `fetch contacts`() = + runTest { + viewModel = ContactsViewModel(repository) + viewModel.getContactsFromSearchParams() + assert(viewModel.contactsViewState.value is ContactsUiState.Success) + val successState = viewModel.contactsViewState.value as ContactsUiState.Success + assert(successState.contacts == FakeItem.contacts) + } + + @Test + fun `test error contacts state`() = + runTest { + viewModel = ContactsViewModel(FakeRepositoryError()) + assert(viewModel.contactsViewState.value is ContactsUiState.Error) + val errorState = viewModel.contactsViewState.value as ContactsUiState.Error + assert(errorState.message == "unable to fetch contacts") + } + + @Test + fun `update search query`() { + viewModel.updateSearchQuery("Ma") + assert(viewModel.searchQuery.value == "Ma") + } + + @Test + fun `initial search query is empty string`() { + viewModel.updateSearchQuery("") + assert(viewModel.searchQuery.value == "") + } + + @Test + fun `initial shareType is User`() { + assert(viewModel.shareTypeList.contains(ShareType.User.shareType)) + } + + @Test + fun `update shareTypes`() { + viewModel.updateShareTypes(listOf(ShareType.Group.shareType)) + assert(viewModel.shareTypeList.contains(ShareType.Group.shareType)) + } + + @Test + fun `initial room state is none`() = + runTest { + assert(viewModel.roomViewState.value is RoomUiState.None) + } + + @Test + fun `test success room state`() = + runTest { + viewModel.createRoom("1", "users", "s@gmail.com", null) + assert(viewModel.roomViewState.value is RoomUiState.Success) + val successState = viewModel.roomViewState.value as RoomUiState.Success + assert(successState.conversation == FakeItem.roomOverall.ocs!!.data) + } + + @Test + fun `test failure room state`() = + runTest { + viewModel = ContactsViewModel(FakeRepositoryError()) + viewModel.createRoom("1", "users", "s@gmail.com", null) + assert(viewModel.roomViewState.value is RoomUiState.Error) + val errorState = viewModel.roomViewState.value as RoomUiState.Error + assert(errorState.message == "unable to create room") + } + + @Test + fun `test image uri`() { + val expectedImageUri = "https://mydomain.com/index.php/avatar/vidya/512" + val imageUri = viewModel.getImageUri("vidya", false) + assert(imageUri == expectedImageUri) + } + + @Test + fun `test error image uri`() { + val expectedImageUri = "https://mydoman.com/index.php/avatar/vidya/512" + val imageUri = viewModel.getImageUri("vidya", false) + assert(imageUri != expectedImageUri) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/contacts/apiService/FakeItem.kt b/app/src/test/java/com/nextcloud/talk/contacts/apiService/FakeItem.kt new file mode 100644 index 0000000..acfcbfe --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/contacts/apiService/FakeItem.kt @@ -0,0 +1,58 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.apiService + +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOCS +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.RoomOCS +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.generic.GenericMeta +import org.mockito.Mockito.mock + +object FakeItem { + val contacts: List = + listOf( + AutocompleteUser(id = "android", label = "Android", source = "users"), + AutocompleteUser(id = "android1", label = "Android 1", source = "users"), + AutocompleteUser(id = "android2", label = "Android 2", source = "users"), + AutocompleteUser(id = "Benny", label = "Benny J", source = "users"), + AutocompleteUser(id = "Benjamin", label = "Benjamin Schmidt", source = "users"), + AutocompleteUser(id = "Chris", label = "Christoph Schmidt", source = "users"), + AutocompleteUser(id = "Daniel", label = "Daniel H", source = "users"), + AutocompleteUser(id = "Dennis", label = "Dennis Richard", source = "users"), + AutocompleteUser(id = "Emma", label = "Emma Jackson", source = "users"), + AutocompleteUser(id = "Emily", label = "Emily Jackson", source = "users"), + AutocompleteUser(id = "Mario", label = "Mario Schmidt", source = "users"), + AutocompleteUser(id = "Maria", label = "Maria Schmidt", source = "users"), + AutocompleteUser(id = "Samsung", label = "Samsung A52", source = "users"), + AutocompleteUser(id = "Tom", label = "Tom Müller", source = "users"), + AutocompleteUser(id = "Tony", label = "Tony Baker", source = "users") + ) + val contactsOverall = AutocompleteOverall( + ocs = AutocompleteOCS( + meta = GenericMeta( + status = "ok", + statusCode = 200, + message = "OK" + ), + data = contacts + ) + ) + val roomOverall: RoomOverall = RoomOverall( + ocs = RoomOCS( + meta = GenericMeta( + status = "ok", + statusCode = 200, + message = "OK" + ), + data = mock(Conversation::class.java) + ) + ) +} diff --git a/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositoryError.kt b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositoryError.kt new file mode 100644 index 0000000..4c8be09 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositoryError.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.repository + +import com.nextcloud.talk.contacts.ContactsRepository +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.conversations.RoomOverall + +class FakeRepositoryError : ContactsRepository { + @Suppress("Detekt.TooGenericExceptionThrown") + override suspend fun getContacts(searchQuery: String?, shareTypes: List): AutocompleteOverall = + throw Exception("unable to fetch contacts") + + @Suppress("Detekt.TooGenericExceptionThrown") + override suspend fun createRoom( + roomType: String, + sourceType: String?, + userId: String, + conversationName: String? + ): RoomOverall = throw Exception("unable to create room") + + override fun getImageUri(avatarId: String, requestBigSize: Boolean) = + "https://mydoman.com/index.php/avatar/$avatarId/512" +} diff --git a/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt new file mode 100644 index 0000000..65a0a3e --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/contacts/repository/FakeRepositorySuccess.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.contacts.repository + +import com.nextcloud.talk.contacts.ContactsRepository +import com.nextcloud.talk.contacts.apiService.FakeItem + +class FakeRepositorySuccess : ContactsRepository { + override suspend fun getContacts(searchQuery: String?, shareTypes: List) = FakeItem.contactsOverall + + override suspend fun createRoom(roomType: String, sourceType: String?, userId: String, conversationName: String?) = + FakeItem.roomOverall + + override fun getImageUri(avatarId: String, requestBigSize: Boolean) = + "https://mydomain.com/index.php/avatar/$avatarId/512" +} diff --git a/app/src/test/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModelTest.kt b/app/src/test/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModelTest.kt new file mode 100644 index 0000000..23d7f41 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/conversationinfo/viewmodel/ConversationInfoViewModelTest.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.conversationinfo.viewmodel + +import org.junit.Test +import org.junit.Assert.assertEquals + +class ConversationInfoViewModelTest { + + @Test + fun `createConversationNameByParticipants should combine names correctly`() { + val original = listOf("Dave", null, "Charlie") + val all = listOf("Bob", "Charlie", "Dave", "Alice", null, "Simon") + + val expectedName = "Charlie, Dave, Alice, Bob, Simon" + val result = ConversationInfoViewModel.createConversationNameByParticipants(original, all) + + assertEquals(expectedName, result) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/json/ConversationConversionTest.kt b/app/src/test/java/com/nextcloud/talk/json/ConversationConversionTest.kt new file mode 100644 index 0000000..c53ad1c --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/json/ConversationConversionTest.kt @@ -0,0 +1,174 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.json + +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.data.database.mappers.asEntity +import com.nextcloud.talk.data.database.mappers.asModel +import com.nextcloud.talk.data.database.model.ConversationEntity +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.participants.Participant +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.io.File + +@RunWith(Parameterized::class) +class ConversationConversionTest(private val jsonFileName: String) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{index}: testDeserialization({0})") + fun data(): List = + listOf( + "RoomOverallExample_APIv1.json", + "RoomOverallExample_APIv2.json", + "RoomOverallExample_APIv4.json" + ) + } + + @Test + fun testDeserialization() { + val jsonFile = File("src/test/resources/$jsonFileName") + val jsonString = jsonFile.readText() + + val roomOverall: RoomOverall = LoganSquare.parse(jsonString, RoomOverall::class.java) + assertNotNull(roomOverall) + + val conversationJson = roomOverall.ocs!!.data!! + assertNotNull(conversationJson) + + val conversationEntity = conversationJson.asEntity(1) + assertNotNull(conversationEntity) + + val apiVersion: Int = jsonFileName.substringAfterLast("APIv").first().digitToInt() + + checkConversationEntity(conversationEntity, apiVersion) + + val conversationModel = conversationEntity.asModel() + val conversationEntityConvertedBack = conversationModel.asEntity() + + checkConversationEntity(conversationEntityConvertedBack, apiVersion) + } + + private fun checkConversationEntity(conversationEntity: ConversationEntity, apiVersion: Int) { + assertEquals("1@juwd77g6", conversationEntity.internalId) + assertEquals(1, conversationEntity.accountId) + + // check if default values are set for the fields when API_V1 is used + if (apiVersion == 1) { + checkConversationEntityV1(conversationEntity) + } + + if (apiVersion >= 1) { + checkConversationEntityLargerThanV1(conversationEntity) + } + + if (apiVersion >= 2) { + assertEquals(false, conversationEntity.canDeleteConversation) + assertEquals(true, conversationEntity.canLeaveConversation) + } + + if (apiVersion >= 3) { + assertEquals("test", conversationEntity.description) + // assertEquals("", conversationEntity.attendeeId) // Not implemented + // assertEquals("", conversationEntity.attendeePin) // Not implemented + assertEquals("users", conversationEntity.actorType) + assertEquals("marcel2", conversationEntity.actorId) + + // assertEquals("", conversationEntity.listable) // Not implemented + assertEquals(0, conversationEntity.callFlag) + // assertEquals("", conversationEntity.sipEnabled) // Not implemented + // assertEquals("", conversationEntity.canEnableSIP) // Not implemented + assertEquals(92320, conversationEntity.lastCommonReadMessage) + } + + if (apiVersion >= 4) { + checkConversationEntityV4(conversationEntity) + } + } + + private fun checkConversationEntityLargerThanV1(conversationEntity: ConversationEntity) { + assertEquals("juwd77g6", conversationEntity.token) + assertEquals(ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL, conversationEntity.type) + assertEquals("marcel", conversationEntity.name) + assertEquals("Marcel", conversationEntity.displayName) + assertEquals(Participant.ParticipantType.OWNER, conversationEntity.participantType) + assertEquals( + ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_WRITE, + conversationEntity.conversationReadOnlyState + ) + assertEquals(1727185155, conversationEntity.lastPing) + assertEquals("0", conversationEntity.sessionId) + assertEquals(false, conversationEntity.hasPassword) + assertEquals(false, conversationEntity.hasCall) + assertEquals(true, conversationEntity.canStartCall) + assertEquals(1727098966, conversationEntity.lastActivity) + assertEquals(false, conversationEntity.favorite) + assertEquals(ConversationEnums.NotificationLevel.ALWAYS, conversationEntity.notificationLevel) + assertEquals(ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS, conversationEntity.lobbyState) + assertEquals(0, conversationEntity.lobbyTimer) + assertEquals(0, conversationEntity.unreadMessages) + assertEquals(false, conversationEntity.unreadMention) + assertEquals(92320, conversationEntity.lastReadMessage) + assertNotNull(conversationEntity.lastMessage) + assertTrue(conversationEntity.lastMessage is String) + assertTrue(conversationEntity.lastMessage!!.contains("token")) + assertEquals(ConversationEnums.ObjectType.DEFAULT, conversationEntity.objectType) + } + + private fun checkConversationEntityV4(conversationEntity: ConversationEntity) { + assertEquals("143a9df3", conversationEntity.avatarVersion) + assertEquals(0, conversationEntity.callStartTime) + assertEquals(0, conversationEntity.callRecording) + assertEquals(false, conversationEntity.unreadMentionDirect) + // assertEquals(, conversationEntity.breakoutRoomMode) // Not implemented + // assertEquals(, conversationEntity.breakoutRoomStatus) // Not implemented + assertEquals("away", conversationEntity.status) + assertEquals("👻", conversationEntity.statusIcon) + assertEquals("buuuuh", conversationEntity.statusMessage) + assertEquals(null, conversationEntity.statusClearAt) + assertEquals("143a9df3", conversationEntity.avatarVersion) + // assertEquals("", conversationEntity.isCustomAvatar) // Not implemented + assertEquals(0, conversationEntity.callStartTime) + assertEquals(0, conversationEntity.callRecording) + // assertEquals("", conversationEntity.recordingConsent) // Not implemented + // assertEquals("", conversationEntity.mentionPermissions) // Not implemented + // assertEquals("", conversationEntity.isArchived) // Not implemented + } + + private fun checkConversationEntityV1(conversationEntity: ConversationEntity) { + // default values for API_V2 fields + assertEquals(false, conversationEntity.canDeleteConversation) + assertEquals(true, conversationEntity.canLeaveConversation) + + // default values for API_V3 fields + assertEquals("", conversationEntity.description) + assertEquals("", conversationEntity.actorType) + assertEquals("", conversationEntity.actorId) + assertEquals(0, conversationEntity.callFlag) + assertEquals(0, conversationEntity.lastCommonReadMessage) + + // default values for API_V4 fields + assertEquals("", conversationEntity.avatarVersion) + assertEquals(0, conversationEntity.callStartTime) + assertEquals(0, conversationEntity.callRecording) + assertEquals(false, conversationEntity.unreadMentionDirect) + assertEquals("", conversationEntity.status) + assertEquals("", conversationEntity.statusIcon) + assertEquals("", conversationEntity.statusMessage) + assertEquals(null, conversationEntity.statusClearAt) + assertEquals("", conversationEntity.avatarVersion) + assertEquals(0, conversationEntity.callStartTime) + assertEquals(0, conversationEntity.callRecording) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt b/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt new file mode 100644 index 0000000..b6bda98 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/login/data/LoginRepositoryTest.kt @@ -0,0 +1,596 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.login.data + +import android.os.Bundle +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.LiveData +import androidx.work.WorkInfo +import com.nextcloud.talk.account.data.LoginRepository +import com.nextcloud.talk.account.data.io.LocalLoginDataSource +import com.nextcloud.talk.account.data.model.LoginCompletion +import com.nextcloud.talk.account.data.model.LoginResponse +import com.nextcloud.talk.account.data.network.NetworkLoginDataSource +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@Suppress("TooManyFunctions", "TooGenericExceptionCaught") +@ExperimentalCoroutinesApi +@RunWith(MockitoJUnitRunner.Silent::class) +class LoginRepositoryTest { + + @get:Rule + val rule = InstantTaskExecutorRule() + + // Repository dependencies + @Mock + lateinit var networkLoginDataSource: NetworkLoginDataSource + + @Mock + lateinit var localLoginDataSource: LocalLoginDataSource + + // Additional mocks for LocalLoginDataSource dependencies + @Mock + lateinit var liveData: LiveData + + lateinit var repo: LoginRepository + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + repo = LoginRepository(networkLoginDataSource, localLoginDataSource) + } + + // ========== pollLogin() Tests ========== + + @Test + fun `pollLogin returns successful LoginCompletion when network returns HTTP 200`() = + runTest { + // Arrange + val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login") + val successfulLoginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + + whenever(networkLoginDataSource.performLoginFlowV2(mockResponse)) + .thenReturn(successfulLoginData) + + // Act + val result = repo.pollLogin(mockResponse) + + // Assert + assertNotNull(result) + assertEquals(200, result?.status) + assertEquals("https://server.com", result?.server) + assertEquals("testuser", result?.loginName) + assertEquals("apppass123", result?.appPassword) + } + + @Test + fun `pollLogin returns null when network returns null`() = + runTest { + // Arrange + val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login") + whenever(networkLoginDataSource.performLoginFlowV2(mockResponse)) + .thenReturn(null) + + // Act + val result = repo.pollLogin(mockResponse) + + // Assert + assertNull(result) + } + + @Test + fun `pollLogin continues polling when status is not HTTP 200 then returns successful result`() = + runTest { + // Arrange + val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login") + val pendingLoginData = LoginCompletion(202, "https://server.com", "testuser", "apppass123") + val successfulLoginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + + whenever(networkLoginDataSource.performLoginFlowV2(mockResponse)) + .thenReturn(pendingLoginData) + .thenReturn(successfulLoginData) + + // Act + val result = repo.pollLogin(mockResponse) + + // Assert + assertNotNull(result) + assertEquals(200, result?.status) + verify(networkLoginDataSource, times(2)).performLoginFlowV2(mockResponse) + } + + @Test + fun `pollLogin handles slow connection by continuing to poll with delays`() = + runTest { + // Arrange + val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login") + val slowResponse1 = LoginCompletion(202, "https://server.com", "testuser", "apppass123") + val slowResponse2 = LoginCompletion(404, "https://server.com", "testuser", "apppass123") + val slowResponse3 = LoginCompletion(500, "https://server.com", "testuser", "apppass123") + val successResponse = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + + whenever(networkLoginDataSource.performLoginFlowV2(mockResponse)) + .thenReturn(slowResponse1) + .thenReturn(slowResponse2) + .thenReturn(slowResponse3) + .thenReturn(successResponse) + + // Act + val result = repo.pollLogin(mockResponse) + + // Assert + assertNotNull(result) + assertEquals(200, result?.status) + verify(networkLoginDataSource, times(4)).performLoginFlowV2(mockResponse) + } + + @Test + fun `pollLogin handles network timeouts during slow connection gracefully`() = + runTest { + // Arrange + val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login") + val timeoutResponse = LoginCompletion(408, "https://server.com", "testuser", "apppass123") + val successResponse = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + + whenever(networkLoginDataSource.performLoginFlowV2(mockResponse)) + .thenReturn(timeoutResponse) + .thenReturn(timeoutResponse) + .thenReturn(successResponse) + + // Act + val result = repo.pollLogin(mockResponse) + + // Assert + assertNotNull(result) + assertEquals(200, result?.status) + verify(networkLoginDataSource, times(3)).performLoginFlowV2(mockResponse) + } + + @Test + fun `pollLogin stops when cancelLoginFlow is called`() = + runTest { + // Arrange + val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login") + val pendingLoginData = LoginCompletion( + 202, + "https://server.com", + "testuser", + "apppass123" + ) + + whenever(networkLoginDataSource.performLoginFlowV2(mockResponse)) + .thenReturn(pendingLoginData) + + // Act - cancel before polling + repo.cancelLoginFlow() + val result = repo.pollLogin(mockResponse) + + // Assert + assertNull(result) + verify(networkLoginDataSource, never()).performLoginFlowV2(any()) + } + + // ========== startLoginFlowFromQR() Tests ========== + + @Test + fun `startLoginFlowFromQR returns LoginCompletion for valid QR data with all parameters`() { + // Arrange + val qrData = "nc://login/user:testuser&server:https%3A//example.com&password:testpass" + + // Act + val result = repo.startLoginFlowFromQR(qrData) + + // Assert + assertNotNull(result) + assertEquals(200, result?.status) + assertEquals("https://example.com", result?.server) + assertEquals("testuser", result?.loginName) + assertEquals("testpass", result?.appPassword) + } + + @Test + fun `startLoginFlowFromQR returns LoginCompletion for minimal valid QR data`() { + // Arrange + val qrData = "nc://login/server:https%3A//example.com" + + // Act + val result = repo.startLoginFlowFromQR(qrData) + + // Assert + assertNull(result) + } + + @Test + fun `startLoginFlowFromQR returns null for invalid prefix`() { + // Arrange + val qrData = "invalid://login/user:testuser&server:https://example.com" + + // Act + val result = repo.startLoginFlowFromQR(qrData) + + // Assert + assertNull(result) + } + + @Test + fun `startLoginFlowFromQR returns null when too many arguments provided`() { + // Arrange + val qrData = "nc://login/user:test&server:https://example.com&password:pass&extra:value" + + // Act + val result = repo.startLoginFlowFromQR(qrData) + + // Assert + assertNull(result) + } + + @Test + fun `startLoginFlowFromQR returns null for empty data`() { + // Arrange + val qrData = "nc://login/" + + // Act + val result = repo.startLoginFlowFromQR(qrData) + + // Assert + assertNull(result) + } + + @Test + fun `startLoginFlowFromQR sets reAuth flag correctly`() { + // Arrange + val qrData = "nc://login/server:https%3A//example.com" + + // Act + val result = repo.startLoginFlowFromQR(qrData, reAuth = true) + + // Assert + assertNull(result) + } + + @Test + fun `startLoginFlowFromQR handles URL encoding correctly`() { + // Arrange + val qrData = "nc://login/user:test%40user.com&server:https%3A//example.com%3A8080&password:test%26pass" + + // Act + val result = repo.startLoginFlowFromQR(qrData) + + // Assert + assertNotNull(result) + assertEquals("test@user.com", result?.loginName) + assertEquals("https://example.com:8080", result?.server) + assertEquals("test&pass", result?.appPassword) + } + + @Test + fun `startLoginFlowFromQR handles mixed parameter order`() { + // Arrange + val qrData = "nc://login/password:testpass&user:testuser&server:https%3A//example.com" + + // Act + val result = repo.startLoginFlowFromQR(qrData) + + // Assert + assertNotNull(result) + assertEquals("https://example.com", result?.server) + assertEquals("testuser", result?.loginName) + assertEquals("testpass", result?.appPassword) + } + + // ========== startLoginFlow() Tests ========== + + @Test + fun `startLoginFlow returns LoginResponse from network`() = + runTest { + // Arrange + val baseUrl = "https://example.com" + val mockResponse = LoginResponse("token123", "https://example.com/poll", "https://example.com/login") + whenever(networkLoginDataSource.anonymouslyPostLoginRequest(baseUrl)) + .thenReturn(mockResponse) + + // Act + val result = repo.startLoginFlow(baseUrl) + + // Assert + assertEquals(mockResponse, result) + verify(networkLoginDataSource).anonymouslyPostLoginRequest(baseUrl) + } + + @Test + fun `startLoginFlow returns null when network returns null`() = + runTest { + // Arrange + val baseUrl = "https://example.com" + whenever(networkLoginDataSource.anonymouslyPostLoginRequest(baseUrl)) + .thenReturn(null) + + // Act + val result = repo.startLoginFlow(baseUrl) + + // Assert + assertNull(result) + } + + @Test + fun `startLoginFlow sets reAuth flag correctly`() = + runTest { + // Arrange + val baseUrl = "https://example.com" + val mockResponse = LoginResponse("token123", "https://example.com/poll", "https://example.com/login") + whenever(networkLoginDataSource.anonymouslyPostLoginRequest(baseUrl)) + .thenReturn(mockResponse) + + // Act + val result = repo.startLoginFlow(baseUrl, reAuth = true) + + // Assert + assertEquals(mockResponse, result) + } + + @Test + fun `startLoginFlow handles network SSL exceptions`() = + runTest { + // Arrange + val baseUrl = "https://example.com" + whenever(networkLoginDataSource.anonymouslyPostLoginRequest(baseUrl)) + .thenReturn(null) // NetworkLoginDataSource catches SSL exceptions and returns null + + // Act + val result = repo.startLoginFlow(baseUrl) + + // Assert + assertNull(result) + verify(networkLoginDataSource).anonymouslyPostLoginRequest(baseUrl) + } + + // ========== cancelLoginFlow() Tests ========== + + @Test + fun `cancelLoginFlow stops polling loop`() = + runTest { + // Arrange + val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login") + val pendingLoginData = LoginCompletion(202, "https://server.com", "testuser", "apppass123") + + whenever(networkLoginDataSource.performLoginFlowV2(mockResponse)) + .thenReturn(pendingLoginData) + + // Act + repo.cancelLoginFlow() + val result = repo.pollLogin(mockResponse) + + // Assert + assertNull(result) + } + + // ========== parseAndLogin() Tests ========== + + @Test + fun `parseAndLogin returns null when user is scheduled for deletion`() { + // Arrange + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(true) + whenever(localLoginDataSource.startAccountRemovalWorker()) + .thenReturn(liveData) + + // Act + val result = repo.parseAndLogin(loginData) + + // Assert + assertNull(result) + verify(localLoginDataSource).startAccountRemovalWorker() + } + + @Test + fun `parseAndLogin returns null when user exists and reAuth is false`() { + // Arrange + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(true) + + // Act + val result = repo.parseAndLogin(loginData) + + // Assert + assertNull(result) + verify(localLoginDataSource, never()).updateUser(any()) + } + + @Test + fun `parseAndLogin updates user when user exists and reAuth is true`() { + // Arrange - First set reAuth to true via QR flow + val qrData = "nc://login/server:https%3A//example.com" + repo.startLoginFlowFromQR(qrData, reAuth = true) + + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(true) + + // Act + val result = repo.parseAndLogin(loginData) + + // Assert + assertNull(result) + verify(localLoginDataSource).updateUser(loginData) + } + + @Test + fun `parseAndLogin returns Bundle for new user with https protocol`() { + // Arrange + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(false) + + // Act + val result = repo.parseAndLogin(loginData) + + // Assert + assertNotNull(result) + assertTrue(result is Bundle) + } + + @Test + fun `parseAndLogin returns Bundle for new user with http protocol`() { + // Arrange + val loginData = LoginCompletion(200, "http://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(false) + + // Act + val result = repo.parseAndLogin(loginData) + + // Assert + assertNotNull(result) + assertTrue(result is Bundle) + } + + @Test + fun `parseAndLogin returns Bundle for new user without protocol prefix`() { + // Arrange + val loginData = LoginCompletion(200, "server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(false) + + // Act + val result = repo.parseAndLogin(loginData) + + // Assert + assertNotNull(result) + assertTrue(result is Bundle) + } + + // ========== LocalLoginDataSource Integration Tests ========== + + @Test + fun `parseAndLogin properly integrates with LocalLoginDataSource checkIfUserIsScheduledForDeletion`() { + // Arrange + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(true) + whenever(localLoginDataSource.startAccountRemovalWorker()) + .thenReturn(liveData) + + // Act + repo.parseAndLogin(loginData) + + // Assert + verify(localLoginDataSource).checkIfUserIsScheduledForDeletion(loginData) + verify(localLoginDataSource).startAccountRemovalWorker() + } + + @Test + fun `parseAndLogin properly integrates with LocalLoginDataSource checkIfUserExists`() { + // Arrange + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(true) + + // Act + repo.parseAndLogin(loginData) + + // Assert + verify(localLoginDataSource).checkIfUserExists(loginData) + verify(localLoginDataSource, never()).updateUser(any()) + } + + @Test + fun `parseAndLogin calls updateUser with correct LoginCompletion data`() { + // Arrange - Set reAuth flag first + val qrData = "nc://login/server:https%3A//example.com" + repo.startLoginFlowFromQR(qrData, reAuth = true) + + val loginData = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + whenever(localLoginDataSource.checkIfUserIsScheduledForDeletion(loginData)) + .thenReturn(false) + whenever(localLoginDataSource.checkIfUserExists(loginData)) + .thenReturn(true) + + // Act + repo.parseAndLogin(loginData) + + // Assert + verify(localLoginDataSource).updateUser(loginData) + } + + // ========== Edge Cases and Error Handling ========== + + @Test + fun `pollLogin handles performLoginFlowV2 returning error status codes`() = + runTest { + // Arrange + val mockResponse = LoginResponse("token123", "https://server.com/poll", "https://server.com/login") + val errorResponse = LoginCompletion(404, "", "", "") + val successResponse = LoginCompletion(200, "https://server.com", "testuser", "apppass123") + + whenever(networkLoginDataSource.performLoginFlowV2(mockResponse)) + .thenReturn(errorResponse) + .thenReturn(successResponse) + + // Act + val result = repo.pollLogin(mockResponse) + + // Assert + assertNotNull(result) + assertEquals(200, result?.status) + } + + @Test + fun `startLoginFlowFromQR handles malformed URL gracefully`() { + // Arrange + val qrData = "nc://login/malformed&data&without&proper&key:value" + + // Act + val result = repo.startLoginFlowFromQR(qrData) + + // Assert + assertNull(result) // Should still create LoginCompletion with empty values + } + + @Test + fun `startLoginFlowFromQR handles partial parameter data`() { + // Arrange + val qrData = "nc://login/user:testuser&server:" + + // Act + val result = repo.startLoginFlowFromQR(qrData) + + // Assert + assertNull(result) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/login/data/network/NetworkLoginDataSourceTest.kt b/app/src/test/java/com/nextcloud/talk/login/data/network/NetworkLoginDataSourceTest.kt new file mode 100644 index 0000000..e83156f --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/login/data/network/NetworkLoginDataSourceTest.kt @@ -0,0 +1,167 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Julius Linus + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.login.data.network + +import com.nextcloud.talk.account.data.model.LoginResponse +import com.nextcloud.talk.account.data.network.NetworkLoginDataSource +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertNull +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.Before +import org.junit.Test +import org.mockito.MockitoAnnotations + +@Suppress("ktlint:standard:max-line-length", "MaxLineLength") +class NetworkLoginDataSourceTest { + + lateinit var network: NetworkLoginDataSource + private val okHttpClient: OkHttpClient = OkHttpClient.Builder().build() + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + network = NetworkLoginDataSource(okHttpClient) + } + + @Test + fun `testing anonymouslyPostLoginRequest correct path`() { + val server = MockWebServer() + server.start(0) + val httpUrl = server.url("index.php/login/v2") + val validResponse = """ + { + "poll":{ + "token":"mQUYQdffOSAMJYtm8pVpkOsVqXt5hglnuSpO5EMbgJMNEPFGaiDe8OUjvrJ2WcYcBSLgqynu9jaPFvZHMl83ybMvp6aDIDARjTFIBpRWod6p32fL9LIpIStvc6k8Wrs1", + "endpoint":"https:\/\/cloud.example.com\/login\/v2\/poll" + }, + "login":"https:\/\/cloud.example.com\/login\/v2\/flow\/guyjGtcKPTKCi4epIRIupIexgJ8wNInMFSfHabACRPZUkmEaWZSM54bFkFuzWksbps7jmTFQjeskLpyJXyhpHlgK8sZBn9HXLXjohIx5iXgJKdOkkZTYCzUWHlsg3YFg" + } + """.trimIndent() + val mockResponse = MockResponse().setBody(validResponse) + server.enqueue(mockResponse) + + val loginResponse = network.anonymouslyPostLoginRequest(httpUrl.toString()) + assertNotNull(loginResponse) + } + + @Test + fun `testing anonymouslyPostLoginRequest error path`() { + val server = MockWebServer() + val invalidResponse = MockResponse() + .addHeader("Content-Type", "application/json; charset=utf-8") + .addHeader("Cache-Control", "no-cache") + .setResponseCode(404) + .setBody("{}") + + server.start() + server.enqueue(invalidResponse) + val httpUrl = server.url("index.php/login/v2") + + val loginResponse = network.anonymouslyPostLoginRequest(httpUrl.toString()) + assertNull(loginResponse) + } + + @Test + fun `testing anonymouslyPostLoginRequest malformed response`() { + val server = MockWebServer() + val validResponse = """ + { + "poll":{ + "token":"mQUYQdffOSAMJYtm8pVpkOsVqXt5hglnuSpO5EMbgJMNEPFGaiDe8OUjvrJ2WcYcBSLgqynu9jaPFvZHMl83ybMvp6aDIDARjTFIBpRWod6p32fL9LIpIStvc6k8Wrs1" + }, + "login":"https:\/\/cloud.example.com\/login\/v2\/flow\/guyjGtcKPTKCi4epIRIupIexgJ8wNInMFSfHabACRPZUkmEaWZSM54bFkFuzWksbps7jmTFQjeskLpyJXyhpHlgK8sZBn9HXLXjohIx5iXgJKdOkkZTYCzUWHlsg3YFg" + } + """.trimIndent() + + val mockResponse = MockResponse().setBody(validResponse) + server.enqueue(mockResponse) + server.start() + val httpUrl = server.url("index.php/login/v2") + + val loginResponse = network.anonymouslyPostLoginRequest(httpUrl.toString()) + assertNull(loginResponse) + } + + @Test + fun `testing performLoginFlowV2 correct path`() { + val server = MockWebServer() + val validBody = """ + { + "server":"https:\/\/cloud.example.com", + "loginName":"username", + "appPassword":"yKTVA4zgxjfivy52WqD8kW3M2pKGQr6srmUXMipRdunxjPFripJn0GMfmtNOqOolYSuJ6sCN" + } + """.trimIndent() + + val validResponse = MockResponse() + .setBody(validBody) + + server.enqueue(validResponse) + server.start() + val httpUrl = server.url("login/v2/poll") + val loginResponse = LoginResponse( + token = "mQUYQdffOSAMJYtm8pVpkOsVqXt5hglnuSpO5EMbgJMNEPFGaiDe8OUjvrJ2WcYcBSLgqynu9jaPFvZHMl83ybMvp6aDIDARjTFIBpRWod6p32fL9LIpIStvc6k8Wrs1", + pollUrl = httpUrl.toString(), + loginUrl = "https:\\/\\/cloud.example.com\\/login\\/v2\\/flow\\/guyjGtcKPTKCi4epIRIupIexgJ8wNInMFSfHabACRPZUkmEaWZSM54bFkFuzWksbps7jmTFQjeskLpyJXyhpHlgK8sZBn9HXLXjohIx5iXgJKdOkkZTYCzUWHlsg3YFg" + ) + + val loginCompletion = network.performLoginFlowV2(loginResponse) + assertNotNull(loginCompletion) + } + + @Test + fun `testing performLoginFlowV2 error path`() { + val server = MockWebServer() + + val invalidResponse = MockResponse() + .addHeader("Content-Type", "application/json; charset=utf-8") + .addHeader("Cache-Control", "no-cache") + .setResponseCode(404) + .setBody("{}") + + server.enqueue(invalidResponse) + server.start() + val httpUrl = server.url("login/v2/poll") + val loginResponse = LoginResponse( + token = "mQUYQdffOSAMJYtm8pVpkOsVqXt5hglnuSpO5EMbgJMNEPFGaiDe8OUjvrJ2WcYcBSLgqynu9jaPFvZHMl83ybMvp6aDIDARjTFIBpRWod6p32fL9LIpIStvc6k8Wrs1", + pollUrl = httpUrl.toString(), + loginUrl = "https:\\/\\/cloud.example.com\\/login\\/v2\\/flow\\/guyjGtcKPTKCi4epIRIupIexgJ8wNInMFSfHabACRPZUkmEaWZSM54bFkFuzWksbps7jmTFQjeskLpyJXyhpHlgK8sZBn9HXLXjohIx5iXgJKdOkkZTYCzUWHlsg3YFg" + ) + + val loginCompletion = network.performLoginFlowV2(loginResponse) + assert(loginCompletion?.status == 404) + } + + @Test + fun `testing performLoginFlowV2 malformed response`() { + val server = MockWebServer() + val validBody = """ + { + "server":"https:\/\/cloud.example.com", + "loginName":"username" + } + """.trimIndent() + + val validResponse = MockResponse() + .setBody(validBody) + + server.enqueue(validResponse) + server.start() + val httpUrl = server.url("login/v2/poll") + val loginResponse = LoginResponse( + token = "mQUYQdffOSAMJYtm8pVpkOsVqXt5hglnuSpO5EMbgJMNEPFGaiDe8OUjvrJ2WcYcBSLgqynu9jaPFvZHMl83ybMvp6aDIDARjTFIBpRWod6p32fL9LIpIStvc6k8Wrs1", + pollUrl = httpUrl.toString(), + loginUrl = "https:\\/\\/cloud.example.com\\/login\\/v2\\/flow\\/guyjGtcKPTKCi4epIRIupIexgJ8wNInMFSfHabACRPZUkmEaWZSM54bFkFuzWksbps7jmTFQjeskLpyJXyhpHlgK8sZBn9HXLXjohIx5iXgJKdOkkZTYCzUWHlsg3YFg" + ) + + val loginCompletion = network.performLoginFlowV2(loginResponse) + assertNull(loginCompletion) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt b/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt new file mode 100644 index 0000000..814c70b --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt @@ -0,0 +1,127 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.messagesearch + +import com.nextcloud.talk.models.domain.SearchMessageEntry +import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository +import com.nextcloud.talk.test.fakes.FakeUnifiedSearchRepository +import io.reactivex.Observable +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.MockitoAnnotations + +class MessageSearchHelperTest { + + val repository = FakeUnifiedSearchRepository() + + @Suppress("LongParameterList") + private fun createMessageEntry( + searchTerm: String = "foo", + thumbnailURL: String = "foo", + title: String = "foo", + messageExcerpt: String = "foo", + conversationToken: String = "foo", + messageId: String? = "foo" + ) = SearchMessageEntry(searchTerm, thumbnailURL, title, messageExcerpt, conversationToken, messageId) + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + } + + @Test + fun emptySearch() { + repository.response = UnifiedSearchRepository.UnifiedSearchResults(0, false, emptyList()) + + val sut = MessageSearchHelper(repository) + + val testObserver = sut.startMessageSearch("foo").test() + testObserver.assertComplete() + testObserver.assertValueCount(1) + val expected = MessageSearchHelper.MessageSearchResults(emptyList(), false) + testObserver.assertValue(expected) + } + + @Test + fun nonEmptySearch_withMoreResults() { + val entries = (1..5).map { createMessageEntry() } + repository.response = UnifiedSearchRepository.UnifiedSearchResults(5, true, entries) + + val sut = MessageSearchHelper(repository) + + val observable = sut.startMessageSearch("foo") + val expected = MessageSearchHelper.MessageSearchResults(entries, true) + testCall(observable, expected) + } + + @Test + fun nonEmptySearch_withNoMoreResults() { + val entries = (1..2).map { createMessageEntry() } + repository.response = UnifiedSearchRepository.UnifiedSearchResults(2, false, entries) + + val sut = MessageSearchHelper(repository) + + val observable = sut.startMessageSearch("foo") + val expected = MessageSearchHelper.MessageSearchResults(entries, false) + testCall(observable, expected) + } + + @Test + fun nonEmptySearch_consecutiveSearches_sameResult() { + val entries = (1..2).map { createMessageEntry() } + repository.response = UnifiedSearchRepository.UnifiedSearchResults(2, false, entries) + + val sut = MessageSearchHelper(repository) + + repeat(5) { + val observable = sut.startMessageSearch("foo") + val expected = MessageSearchHelper.MessageSearchResults(entries, false) + testCall(observable, expected) + } + } + + @Test + fun loadMore_noPreviousResults() { + val sut = MessageSearchHelper(repository) + Assert.assertEquals(null, sut.loadMore()) + } + + @Test + fun loadMore_previousResults_sameSearch() { + val sut = MessageSearchHelper(repository) + + val firstPageEntries = (1..5).map { createMessageEntry() } + repository.response = UnifiedSearchRepository.UnifiedSearchResults(5, true, firstPageEntries) + + val firstPageObservable = sut.startMessageSearch("foo") + Assert.assertEquals(0, repository.lastRequestedCursor) + val firstPageExpected = MessageSearchHelper.MessageSearchResults(firstPageEntries, true) + testCall(firstPageObservable, firstPageExpected) + + val secondPageEntries = (1..5).map { createMessageEntry(title = "bar") } + repository.response = UnifiedSearchRepository.UnifiedSearchResults(10, false, secondPageEntries) + + val secondPageObservable = sut.loadMore() + Assert.assertEquals(5, repository.lastRequestedCursor) + Assert.assertNotNull(secondPageObservable) + val secondPageExpected = MessageSearchHelper.MessageSearchResults(firstPageEntries + secondPageEntries, false) + testCall(secondPageObservable!!, secondPageExpected) + } + + private fun testCall( + searchCall: Observable, + expectedResult: MessageSearchHelper.MessageSearchResults + ) { + val testObserver = searchCall.test() + testObserver.assertComplete() + testObserver.assertValueCount(1) + testObserver.assertValue(expectedResult) + testObserver.dispose() + } +} diff --git a/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverCallParticipantTest.java b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverCallParticipantTest.java new file mode 100644 index 0000000..a7ad80d --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverCallParticipantTest.java @@ -0,0 +1,262 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import com.nextcloud.talk.models.json.signaling.NCMessagePayload; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +public class SignalingMessageReceiverCallParticipantTest { + + private SignalingMessageReceiver signalingMessageReceiver; + + @Before + public void setUp() { + // SignalingMessageReceiver is abstract to prevent direct instantiation without calling the appropriate + // protected methods. + signalingMessageReceiver = new SignalingMessageReceiver() { + }; + } + + @Test + public void testAddCallParticipantMessageListenerWithNullListener() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener(null, "theSessionId"); + }); + } + + @Test + public void testAddCallParticipantMessageListenerWithNullSessionId() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, null); + }); + } + + @Test + public void testCallParticipantMessageRaiseHand() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("raiseHand"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("raiseHand"); + messagePayload.setState(Boolean.TRUE); + messagePayload.setTimestamp(4815162342L); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedCallParticipantMessageListener, only()).onRaiseHand(true, 4815162342L); + } + + @Test + public void testCallParticipantMessageReaction() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("reaction"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("reaction"); + messagePayload.setReaction("theReaction"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedCallParticipantMessageListener, only()).onReaction("theReaction"); + } + + @Test + public void testCallParticipantMessageUnshareScreen() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("unshareScreen"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedCallParticipantMessageListener, only()).onUnshareScreen(); + } + + @Test + public void testCallParticipantMessageSeveralListenersSameFrom() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId"); + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("unshareScreen"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedCallParticipantMessageListener1, only()).onUnshareScreen(); + verify(mockedCallParticipantMessageListener2, only()).onUnshareScreen(); + } + + @Test + public void testCallParticipantMessageNotMatchingSessionId() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("notMatchingSessionId"); + signalingMessage.setType("unshareScreen"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedCallParticipantMessageListener); + } + + @Test + public void testCallParticipantMessageAfterRemovingListener() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId"); + signalingMessageReceiver.removeListener(mockedCallParticipantMessageListener); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("unshareScreen"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedCallParticipantMessageListener); + } + + @Test + public void testCallParticipantMessageAfterRemovingSingleListenerOfSeveral() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener3 = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId"); + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId"); + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener3, "theSessionId"); + signalingMessageReceiver.removeListener(mockedCallParticipantMessageListener2); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("unshareScreen"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedCallParticipantMessageListener1, only()).onUnshareScreen(); + verify(mockedCallParticipantMessageListener3, only()).onUnshareScreen(); + verifyNoInteractions(mockedCallParticipantMessageListener2); + } + + @Test + public void testCallParticipantMessageAfterAddingListenerAgainForDifferentFrom() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId"); + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId2"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("unshareScreen"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedCallParticipantMessageListener); + + signalingMessage.setFrom("theSessionId2"); + signalingMessage.setType("unshareScreen"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedCallParticipantMessageListener, only()).onUnshareScreen(); + } + + @Test + public void testAddCallParticipantMessageListenerWhenHandlingCallParticipantMessage() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId"); + return null; + }).when(mockedCallParticipantMessageListener1).onUnshareScreen(); + + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("unshareScreen"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedCallParticipantMessageListener1, only()).onUnshareScreen(); + verifyNoInteractions(mockedCallParticipantMessageListener2); + } + + @Test + public void testRemoveCallParticipantMessageListenerWhenHandlingCallParticipantMessage() { + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 = + mock(SignalingMessageReceiver.CallParticipantMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.removeListener(mockedCallParticipantMessageListener2); + return null; + }).when(mockedCallParticipantMessageListener1).onUnshareScreen(); + + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId"); + signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("unshareScreen"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + InOrder inOrder = inOrder(mockedCallParticipantMessageListener1, mockedCallParticipantMessageListener2); + + inOrder.verify(mockedCallParticipantMessageListener1).onUnshareScreen(); + inOrder.verify(mockedCallParticipantMessageListener2).onUnshareScreen(); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverLocalParticipantTest.java b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverLocalParticipantTest.java new file mode 100644 index 0000000..5137619 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverLocalParticipantTest.java @@ -0,0 +1,180 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +public class SignalingMessageReceiverLocalParticipantTest { + + private SignalingMessageReceiver signalingMessageReceiver; + + @Before + public void setUp() { + // SignalingMessageReceiver is abstract to prevent direct instantiation without calling the appropriate + // protected methods. + signalingMessageReceiver = new SignalingMessageReceiver() { + }; + } + + @Test + public void testAddLocalParticipantMessageListenerWithNullListener() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener((SignalingMessageReceiver.LocalParticipantMessageListener) null); + }); + } + + @Test + public void testExternalSignalingLocalParticipantMessageSwitchTo() { + SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener = + mock(SignalingMessageReceiver.LocalParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "switchto"); + eventMap.put("target", "room"); + Map switchToMap = new HashMap<>(); + switchToMap.put("roomid", "theToken"); + eventMap.put("switchto", switchToMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedLocalParticipantMessageListener, only()).onSwitchTo("theToken"); + } + + @Test + public void testExternalSignalingLocalParticipantMessageAfterRemovingListener() { + SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener = + mock(SignalingMessageReceiver.LocalParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener); + signalingMessageReceiver.removeListener(mockedLocalParticipantMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "switchto"); + eventMap.put("target", "room"); + HashMap switchToMap = new HashMap<>(); + switchToMap.put("roomid", "theToken"); + eventMap.put("switchto", switchToMap); + signalingMessageReceiver.processEvent(eventMap); + + verifyNoInteractions(mockedLocalParticipantMessageListener); + } + + @Test + public void testExternalSignalingLocalParticipantMessageAfterRemovingSingleListenerOfSeveral() { + SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener1 = + mock(SignalingMessageReceiver.LocalParticipantMessageListener.class); + SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener2 = + mock(SignalingMessageReceiver.LocalParticipantMessageListener.class); + SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener3 = + mock(SignalingMessageReceiver.LocalParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener1); + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener2); + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener3); + signalingMessageReceiver.removeListener(mockedLocalParticipantMessageListener2); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "switchto"); + eventMap.put("target", "room"); + HashMap switchToMap = new HashMap<>(); + switchToMap.put("roomid", "theToken"); + eventMap.put("switchto", switchToMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedLocalParticipantMessageListener1, only()).onSwitchTo("theToken"); + verify(mockedLocalParticipantMessageListener3, only()).onSwitchTo("theToken"); + verifyNoInteractions(mockedLocalParticipantMessageListener2); + } + + @Test + public void testExternalSignalingLocalParticipantMessageAfterAddingListenerAgain() { + SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener = + mock(SignalingMessageReceiver.LocalParticipantMessageListener.class); + + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener); + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "switchto"); + eventMap.put("target", "room"); + HashMap switchToMap = new HashMap<>(); + switchToMap.put("roomid", "theToken"); + eventMap.put("switchto", switchToMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedLocalParticipantMessageListener, only()).onSwitchTo("theToken"); + } + + @Test + public void testAddLocalParticipantMessageListenerWhenHandlingExternalSignalingLocalParticipantMessage() { + SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener1 = + mock(SignalingMessageReceiver.LocalParticipantMessageListener.class); + SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener2 = + mock(SignalingMessageReceiver.LocalParticipantMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener2); + return null; + }).when(mockedLocalParticipantMessageListener1).onSwitchTo("theToken"); + + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener1); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "switchto"); + eventMap.put("target", "room"); + HashMap switchToMap = new HashMap<>(); + switchToMap.put("roomid", "theToken"); + eventMap.put("switchto", switchToMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedLocalParticipantMessageListener1, only()).onSwitchTo("theToken"); + verifyNoInteractions(mockedLocalParticipantMessageListener2); + } + + @Test + public void testRemoveLocalParticipantMessageListenerWhenHandlingExternalSignalingLocalParticipantMessage() { + SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener1 = + mock(SignalingMessageReceiver.LocalParticipantMessageListener.class); + SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener2 = + mock(SignalingMessageReceiver.LocalParticipantMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.removeListener(mockedLocalParticipantMessageListener2); + return null; + }).when(mockedLocalParticipantMessageListener1).onSwitchTo("theToken"); + + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener1); + signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener2); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "switchto"); + eventMap.put("target", "room"); + HashMap switchToMap = new HashMap<>(); + switchToMap.put("roomid", "theToken"); + eventMap.put("switchto", switchToMap); + signalingMessageReceiver.processEvent(eventMap); + + InOrder inOrder = inOrder(mockedLocalParticipantMessageListener1, mockedLocalParticipantMessageListener2); + + inOrder.verify(mockedLocalParticipantMessageListener1).onSwitchTo("theToken"); + inOrder.verify(mockedLocalParticipantMessageListener2).onSwitchTo("theToken"); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverOfferTest.java b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverOfferTest.java new file mode 100644 index 0000000..da279e4 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverOfferTest.java @@ -0,0 +1,218 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import com.nextcloud.talk.models.json.signaling.NCMessagePayload; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +public class SignalingMessageReceiverOfferTest { + + private SignalingMessageReceiver signalingMessageReceiver; + + @Before + public void setUp() { + // SignalingMessageReceiver is abstract to prevent direct instantiation without calling the appropriate + // protected methods. + signalingMessageReceiver = new SignalingMessageReceiver() { + }; + } + + @Test + public void testAddOfferMessageListenerWithNullListener() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener((SignalingMessageReceiver.OfferMessageListener) null); + }); + } + + @Test + public void testOfferMessage() { + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener = + mock(SignalingMessageReceiver.OfferMessageListener.class); + + signalingMessageReceiver.addListener(mockedOfferMessageListener); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedOfferMessageListener, only()).onOffer("theSessionId", "theRoomType", "theSdp", null); + } + + @Test + public void testOfferMessageWithNick() { + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener = + mock(SignalingMessageReceiver.OfferMessageListener.class); + + signalingMessageReceiver.addListener(mockedOfferMessageListener); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedOfferMessageListener, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + } + + @Test + public void testOfferMessageAfterRemovingListener() { + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener = + mock(SignalingMessageReceiver.OfferMessageListener.class); + + signalingMessageReceiver.addListener(mockedOfferMessageListener); + signalingMessageReceiver.removeListener(mockedOfferMessageListener); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedOfferMessageListener); + } + + @Test + public void testOfferMessageAfterRemovingSingleListenerOfSeveral() { + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener1 = + mock(SignalingMessageReceiver.OfferMessageListener.class); + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener2 = + mock(SignalingMessageReceiver.OfferMessageListener.class); + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener3 = + mock(SignalingMessageReceiver.OfferMessageListener.class); + + signalingMessageReceiver.addListener(mockedOfferMessageListener1); + signalingMessageReceiver.addListener(mockedOfferMessageListener2); + signalingMessageReceiver.addListener(mockedOfferMessageListener3); + signalingMessageReceiver.removeListener(mockedOfferMessageListener2); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedOfferMessageListener1, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + verify(mockedOfferMessageListener3, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + verifyNoInteractions(mockedOfferMessageListener2); + } + + @Test + public void testOfferMessageAfterAddingListenerAgain() { + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener = + mock(SignalingMessageReceiver.OfferMessageListener.class); + + signalingMessageReceiver.addListener(mockedOfferMessageListener); + signalingMessageReceiver.addListener(mockedOfferMessageListener); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedOfferMessageListener, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + } + + @Test + public void testAddOfferMessageListenerWhenHandlingOffer() { + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener1 = + mock(SignalingMessageReceiver.OfferMessageListener.class); + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener2 = + mock(SignalingMessageReceiver.OfferMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.addListener(mockedOfferMessageListener2); + return null; + }).when(mockedOfferMessageListener1).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + + signalingMessageReceiver.addListener(mockedOfferMessageListener1); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedOfferMessageListener1, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + verifyNoInteractions(mockedOfferMessageListener2); + } + + @Test + public void testRemoveOfferMessageListenerWhenHandlingOffer() { + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener1 = + mock(SignalingMessageReceiver.OfferMessageListener.class); + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener2 = + mock(SignalingMessageReceiver.OfferMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.removeListener(mockedOfferMessageListener2); + return null; + }).when(mockedOfferMessageListener1).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + + signalingMessageReceiver.addListener(mockedOfferMessageListener1); + signalingMessageReceiver.addListener(mockedOfferMessageListener2); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + InOrder inOrder = inOrder(mockedOfferMessageListener1, mockedOfferMessageListener2); + + inOrder.verify(mockedOfferMessageListener1).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + inOrder.verify(mockedOfferMessageListener2).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverParticipantListTest.java b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverParticipantListTest.java new file mode 100644 index 0000000..85214e5 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverParticipantListTest.java @@ -0,0 +1,461 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import com.nextcloud.talk.models.json.participants.Participant; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +public class SignalingMessageReceiverParticipantListTest { + + private SignalingMessageReceiver signalingMessageReceiver; + + @Before + public void setUp() { + // SignalingMessageReceiver is abstract to prevent direct instantiation without calling the appropriate + // protected methods. + signalingMessageReceiver = new SignalingMessageReceiver() { + }; + } + + @Test + public void testAddParticipantListMessageListenerWithNullListener() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener((SignalingMessageReceiver.ParticipantListMessageListener) null); + }); + } + + @Test + public void testInternalSignalingParticipantListMessageUsersInRoom() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + + List> users = new ArrayList<>(2); + Map user1 = new HashMap<>(); + user1.put("inCall", 7); + user1.put("lastPing", 4815); + user1.put("roomId", 108); + user1.put("sessionId", "theSessionId1"); + user1.put("userId", "theUserId"); + // If any of the following properties is set in any of the participants all the other participants in the + // message would have it too. But for test simplicity, and as it is not relevant for the processing, in this + // test they are included only in one of the participants. + user1.put("participantPermissions", 42); + user1.put("actorType", "federated_users"); + user1.put("actorId", "theActorId"); + users.add(user1); + Map user2 = new HashMap<>(); + user2.put("inCall", 0); + user2.put("lastPing", 162342); + user2.put("roomId", 108); + user2.put("sessionId", "theSessionId2"); + user2.put("userId", ""); + users.add(user2); + signalingMessageReceiver.processUsersInRoom(users); + + List expectedParticipantList = new ArrayList<>(); + Participant expectedParticipant1 = new Participant(); + expectedParticipant1.setInCall(Participant.InCallFlags.IN_CALL | Participant.InCallFlags.WITH_AUDIO | Participant.InCallFlags.WITH_VIDEO); + expectedParticipant1.setLastPing(4815); + expectedParticipant1.setSessionId("theSessionId1"); + expectedParticipant1.setUserId("theUserId"); + expectedParticipant1.setActorType(Participant.ActorType.FEDERATED); + expectedParticipant1.setActorId("theActorId"); + expectedParticipantList.add(expectedParticipant1); + + Participant expectedParticipant2 = new Participant(); + expectedParticipant2.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant2.setLastPing(162342); + expectedParticipant2.setSessionId("theSessionId2"); + expectedParticipantList.add(expectedParticipant2); + + verify(mockedParticipantListMessageListener, only()).onUsersInRoom(expectedParticipantList); + } + + @Test + public void testInternalSignalingParticipantListMessageAfterRemovingListener() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener); + + List> users = new ArrayList<>(1); + Map user = new HashMap<>(); + user.put("inCall", 0); + user.put("lastPing", 4815); + user.put("roomId", 108); + user.put("sessionId", "theSessionId"); + user.put("userId", ""); + users.add(user); + signalingMessageReceiver.processUsersInRoom(users); + + verifyNoInteractions(mockedParticipantListMessageListener); + } + + @Test + public void testInternalSignalingParticipantListMessageAfterRemovingSingleListenerOfSeveral() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener3 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener3); + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2); + + List> users = new ArrayList<>(1); + Map user = new HashMap<>(); + user.put("inCall", 0); + user.put("lastPing", 4815); + user.put("roomId", 108); + user.put("sessionId", "theSessionId"); + user.put("userId", ""); + users.add(user); + signalingMessageReceiver.processUsersInRoom(users); + + List expectedParticipantList = new ArrayList<>(); + Participant expectedParticipant = new Participant(); + expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant.setLastPing(4815); + expectedParticipant.setSessionId("theSessionId"); + expectedParticipantList.add(expectedParticipant); + + verify(mockedParticipantListMessageListener1, only()).onUsersInRoom(expectedParticipantList); + verify(mockedParticipantListMessageListener3, only()).onUsersInRoom(expectedParticipantList); + verifyNoInteractions(mockedParticipantListMessageListener2); + } + + @Test + public void testInternalSignalingParticipantListMessageAfterAddingListenerAgain() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + + List> users = new ArrayList<>(1); + Map user = new HashMap<>(); + user.put("inCall", 0); + user.put("lastPing", 4815); + user.put("roomId", 108); + user.put("sessionId", "theSessionId"); + user.put("userId", ""); + users.add(user); + signalingMessageReceiver.processUsersInRoom(users); + + List expectedParticipantList = new ArrayList<>(); + Participant expectedParticipant = new Participant(); + expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant.setLastPing(4815); + expectedParticipant.setSessionId("theSessionId"); + expectedParticipantList.add(expectedParticipant); + + verify(mockedParticipantListMessageListener, only()).onUsersInRoom(expectedParticipantList); + } + + @Test + public void testAddParticipantListMessageListenerWhenHandlingInternalSignalingParticipantListMessage() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + List expectedParticipantList = new ArrayList<>(); + Participant expectedParticipant = new Participant(); + expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant.setLastPing(4815); + expectedParticipant.setSessionId("theSessionId"); + expectedParticipantList.add(expectedParticipant); + + doAnswer((invocation) -> { + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + return null; + }).when(mockedParticipantListMessageListener1).onUsersInRoom(expectedParticipantList); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + + List> users = new ArrayList<>(1); + Map user = new HashMap<>(); + user.put("inCall", 0); + user.put("lastPing", 4815); + user.put("roomId", 108); + user.put("sessionId", "theSessionId"); + user.put("userId", ""); + users.add(user); + signalingMessageReceiver.processUsersInRoom(users); + + verify(mockedParticipantListMessageListener1, only()).onUsersInRoom(expectedParticipantList); + verifyNoInteractions(mockedParticipantListMessageListener2); + } + + @Test + public void testRemoveParticipantListMessageListenerWhenHandlingInternalSignalingParticipantListMessage() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + List expectedParticipantList = new ArrayList<>(); + Participant expectedParticipant = new Participant(); + expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant.setLastPing(4815); + expectedParticipant.setSessionId("theSessionId"); + expectedParticipantList.add(expectedParticipant); + + doAnswer((invocation) -> { + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2); + return null; + }).when(mockedParticipantListMessageListener1).onUsersInRoom(expectedParticipantList); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + + List> users = new ArrayList<>(1); + Map user = new HashMap<>(); + user.put("inCall", 0); + user.put("lastPing", 4815); + user.put("roomId", 108); + user.put("sessionId", "theSessionId"); + user.put("userId", ""); + users.add(user); + signalingMessageReceiver.processUsersInRoom(users); + + InOrder inOrder = inOrder(mockedParticipantListMessageListener1, mockedParticipantListMessageListener2); + + inOrder.verify(mockedParticipantListMessageListener1).onUsersInRoom(expectedParticipantList); + inOrder.verify(mockedParticipantListMessageListener2).onUsersInRoom(expectedParticipantList); + } + + @Test + public void testExternalSignalingParticipantListMessageParticipantsUpdate() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + Map updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + List> users = new ArrayList<>(2); + Map user1 = new HashMap<>(); + user1.put("inCall", 7); + user1.put("lastPing", 4815); + user1.put("sessionId", "theSessionId1"); + user1.put("participantType", 3); + user1.put("userId", "theUserId"); + // If any of the following properties is set in any of the participants all the other participants in the + // message would have it too. But for test simplicity, and as it is not relevant for the processing, in this + // test they are included only in one of the participants. + user1.put("nextcloudSessionId", "theNextcloudSessionId"); + user1.put("participantPermissions", 42); + user1.put("actorType", "federated_users"); + user1.put("actorId", "theActorId"); + users.add(user1); + Map user2 = new HashMap<>(); + user2.put("inCall", 0); + user2.put("lastPing", 162342); + user2.put("sessionId", "theSessionId2"); + user2.put("participantType", 4); + users.add(user2); + updateMap.put("users", users); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + List expectedParticipantList = new ArrayList<>(2); + Participant expectedParticipant1 = new Participant(); + expectedParticipant1.setInCall(Participant.InCallFlags.IN_CALL | Participant.InCallFlags.WITH_AUDIO | Participant.InCallFlags.WITH_VIDEO); + expectedParticipant1.setLastPing(4815); + expectedParticipant1.setSessionId("theSessionId1"); + expectedParticipant1.setType(Participant.ParticipantType.USER); + expectedParticipant1.setUserId("theUserId"); + expectedParticipant1.setActorType(Participant.ActorType.FEDERATED); + expectedParticipant1.setActorId("theActorId"); + expectedParticipantList.add(expectedParticipant1); + + Participant expectedParticipant2 = new Participant(); + expectedParticipant2.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant2.setLastPing(162342); + expectedParticipant2.setSessionId("theSessionId2"); + expectedParticipant2.setType(Participant.ParticipantType.GUEST); + expectedParticipantList.add(expectedParticipant2); + + verify(mockedParticipantListMessageListener, only()).onParticipantsUpdate(expectedParticipantList); + } + + @Test + public void testExternalSignalingParticipantListMessageAllParticipantsUpdate() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + Map updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("incall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedParticipantListMessageListener, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + } + + @Test + public void testExternalSignalingParticipantListMessageAfterRemovingListener() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + HashMap updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("incall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + verifyNoInteractions(mockedParticipantListMessageListener); + } + + @Test + public void testExternalSignalingParticipantListMessageAfterRemovingSingleListenerOfSeveral() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener3 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener3); + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + HashMap updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("incall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedParticipantListMessageListener1, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + verify(mockedParticipantListMessageListener3, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + verifyNoInteractions(mockedParticipantListMessageListener2); + } + + @Test + public void testExternalSignalingParticipantListMessageAfterAddingListenerAgain() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + HashMap updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("incall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedParticipantListMessageListener, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + } + + @Test + public void testAddParticipantListMessageListenerWhenHandlingExternalSignalingParticipantListMessage() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + return null; + }).when(mockedParticipantListMessageListener1).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + HashMap updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("incall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedParticipantListMessageListener1, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + verifyNoInteractions(mockedParticipantListMessageListener2); + } + + @Test + public void testRemoveParticipantListMessageListenerWhenHandlingExternalSignalingParticipantListMessage() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2); + return null; + }).when(mockedParticipantListMessageListener1).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + HashMap updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("incall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + InOrder inOrder = inOrder(mockedParticipantListMessageListener1, mockedParticipantListMessageListener2); + + inOrder.verify(mockedParticipantListMessageListener1).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + inOrder.verify(mockedParticipantListMessageListener2).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverTest.java b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverTest.java new file mode 100644 index 0000000..c3e8154 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverTest.java @@ -0,0 +1,122 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import com.nextcloud.talk.models.json.signaling.NCMessagePayload; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +public class SignalingMessageReceiverTest { + + private SignalingMessageReceiver signalingMessageReceiver; + + @Before + public void setUp() { + // SignalingMessageReceiver is abstract to prevent direct instantiation without calling the appropriate + // protected methods. + signalingMessageReceiver = new SignalingMessageReceiver() { + }; + } + + @Test + public void testOfferWithOfferAndWebRtcMessageListeners() { + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener = + mock(SignalingMessageReceiver.OfferMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedOfferMessageListener); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + InOrder inOrder = inOrder(mockedOfferMessageListener, mockedWebRtcMessageListener); + + inOrder.verify(mockedOfferMessageListener).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + inOrder.verify(mockedWebRtcMessageListener).onOffer("theSdp", "theNick"); + } + + @Test + public void testAddWebRtcMessageListenerWhenHandlingOffer() { + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener = + mock(SignalingMessageReceiver.OfferMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + return null; + }).when(mockedOfferMessageListener).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + + signalingMessageReceiver.addListener(mockedOfferMessageListener); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + InOrder inOrder = inOrder(mockedOfferMessageListener, mockedWebRtcMessageListener); + + inOrder.verify(mockedOfferMessageListener).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + inOrder.verify(mockedWebRtcMessageListener).onOffer("theSdp", "theNick"); + } + + @Test + public void testRemoveWebRtcMessageListenerWhenHandlingOffer() { + SignalingMessageReceiver.OfferMessageListener mockedOfferMessageListener = + mock(SignalingMessageReceiver.OfferMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.removeListener(mockedWebRtcMessageListener); + return null; + }).when(mockedOfferMessageListener).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + + signalingMessageReceiver.addListener(mockedOfferMessageListener); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedOfferMessageListener, only()).onOffer("theSessionId", "theRoomType", "theSdp", "theNick"); + verifyNoInteractions(mockedWebRtcMessageListener); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverWebRtcTest.java b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverWebRtcTest.java new file mode 100644 index 0000000..8422a12 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverWebRtcTest.java @@ -0,0 +1,353 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.signaling; + +import com.nextcloud.talk.models.json.signaling.NCIceCandidate; +import com.nextcloud.talk.models.json.signaling.NCMessagePayload; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +public class SignalingMessageReceiverWebRtcTest { + + private SignalingMessageReceiver signalingMessageReceiver; + + @Before + public void setUp() { + // SignalingMessageReceiver is abstract to prevent direct instantiation without calling the appropriate + // protected methods. + signalingMessageReceiver = new SignalingMessageReceiver() { + }; + } + + @Test + public void testAddWebRtcMessageListenerWithNullListener() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener(null, "theSessionId", "theRoomType"); + }); + } + + @Test + public void testAddWebRtcMessageListenerWithNullSessionId() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, null, "theRoomType"); + }); + } + + @Test + public void testAddWebRtcMessageListenerWithNullRoomType() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", null); + }); + } + + @Test + public void testWebRtcMessageOffer() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onOffer("theSdp", null); + } + + @Test + public void testWebRtcMessageOfferWithNick() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onOffer("theSdp", "theNick"); + } + + @Test + public void testWebRtcMessageAnswer() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("answer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("answer"); + messagePayload.setSdp("theSdp"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onAnswer("theSdp", null); + } + + @Test + public void testWebRtcMessageAnswerWithNick() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("answer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("answer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onAnswer("theSdp", "theNick"); + } + + @Test + public void testWebRtcMessageCandidate() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("candidate"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + NCIceCandidate iceCandidate = new NCIceCandidate(); + iceCandidate.setSdpMid("theSdpMid"); + iceCandidate.setSdpMLineIndex(42); + iceCandidate.setCandidate("theSdp"); + messagePayload.setIceCandidate(iceCandidate); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onCandidate("theSdpMid", 42, "theSdp"); + } + + @Test + public void testWebRtcMessageEndOfCandidates() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onEndOfCandidates(); + } + + @Test + public void testWebRtcMessageSeveralListenersSameFrom() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType"); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener1, only()).onEndOfCandidates(); + verify(mockedWebRtcMessageListener2, only()).onEndOfCandidates(); + } + + @Test + public void testWebRtcMessageNotMatchingSessionId() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("notMatchingSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedWebRtcMessageListener); + } + + @Test + public void testWebRtcMessageNotMatchingRoomType() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("notMatchingRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedWebRtcMessageListener); + } + + @Test + public void testWebRtcMessageAfterRemovingListener() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + signalingMessageReceiver.removeListener(mockedWebRtcMessageListener); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedWebRtcMessageListener); + } + + @Test + public void testWebRtcMessageAfterRemovingSingleListenerOfSeveral() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener3 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType"); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType"); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener3, "theSessionId", "theRoomType"); + signalingMessageReceiver.removeListener(mockedWebRtcMessageListener2); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener1, only()).onEndOfCandidates(); + verify(mockedWebRtcMessageListener3, only()).onEndOfCandidates(); + verifyNoInteractions(mockedWebRtcMessageListener2); + } + + @Test + public void testWebRtcMessageAfterAddingListenerAgainForDifferentFrom() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId2", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedWebRtcMessageListener); + + signalingMessage.setFrom("theSessionId2"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onEndOfCandidates(); + } + + @Test + public void testAddWebRtcMessageListenerWhenHandlingWebRtcMessage() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType"); + return null; + }).when(mockedWebRtcMessageListener1).onEndOfCandidates(); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener1, only()).onEndOfCandidates(); + verifyNoInteractions(mockedWebRtcMessageListener2); + } + + @Test + public void testRemoveWebRtcMessageListenerWhenHandlingWebRtcMessage() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.removeListener(mockedWebRtcMessageListener2); + return null; + }).when(mockedWebRtcMessageListener1).onEndOfCandidates(); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType"); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + InOrder inOrder = inOrder(mockedWebRtcMessageListener1, mockedWebRtcMessageListener2); + + inOrder.verify(mockedWebRtcMessageListener1).onEndOfCandidates(); + inOrder.verify(mockedWebRtcMessageListener2).onEndOfCandidates(); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/test/fakes/FakeCallRecordingRepository.kt b/app/src/test/java/com/nextcloud/talk/test/fakes/FakeCallRecordingRepository.kt new file mode 100644 index 0000000..d7af5eb --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/test/fakes/FakeCallRecordingRepository.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.test.fakes + +import com.nextcloud.talk.models.domain.StartCallRecordingModel +import com.nextcloud.talk.models.domain.StopCallRecordingModel +import com.nextcloud.talk.repositories.callrecording.CallRecordingRepository +import io.reactivex.Observable + +class FakeCallRecordingRepository : CallRecordingRepository { + + override fun startRecording(roomToken: String) = Observable.just(StartCallRecordingModel(true)) + + override fun stopRecording(roomToken: String) = Observable.just(StopCallRecordingModel(true)) +} diff --git a/app/src/test/java/com/nextcloud/talk/test/fakes/FakeUnifiedSearchRepository.kt b/app/src/test/java/com/nextcloud/talk/test/fakes/FakeUnifiedSearchRepository.kt new file mode 100644 index 0000000..e3ecc05 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/test/fakes/FakeUnifiedSearchRepository.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.test.fakes + +import com.nextcloud.talk.models.domain.SearchMessageEntry +import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository +import io.reactivex.Observable + +class FakeUnifiedSearchRepository : UnifiedSearchRepository { + + lateinit var response: UnifiedSearchRepository.UnifiedSearchResults + var lastRequestedCursor = -1 + + override fun searchMessages( + searchTerm: String, + cursor: Int, + limit: Int + ): Observable> { + lastRequestedCursor = cursor + return Observable.just(response) + } + + override fun searchInRoom( + roomToken: String, + searchTerm: String, + cursor: Int, + limit: Int + ): Observable> { + lastRequestedCursor = cursor + return Observable.just(response) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/utils/BundleKeysTest.kt b/app/src/test/java/com/nextcloud/talk/utils/BundleKeysTest.kt new file mode 100644 index 0000000..1677e04 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/utils/BundleKeysTest.kt @@ -0,0 +1,90 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Samanwith KSN + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.utils.bundle.BundleKeys +import junit.framework.TestCase.assertEquals +import org.junit.Test +class BundleKeysTest { + + @Test + fun testBundleKeysValues() { + assertEquals("KEY_SELECTED_USERS", BundleKeys.KEY_SELECTED_USERS) + assertEquals("KEY_SELECTED_GROUPS", BundleKeys.KEY_SELECTED_GROUPS) + assertEquals("KEY_SELECTED_CIRCLES", BundleKeys.KEY_SELECTED_CIRCLES) + assertEquals("KEY_SELECTED_EMAILS", BundleKeys.KEY_SELECTED_EMAILS) + assertEquals("KEY_USERNAME", BundleKeys.KEY_USERNAME) + assertEquals("KEY_TOKEN", BundleKeys.KEY_TOKEN) + assertEquals("KEY_TRANSLATE_MESSAGE", BundleKeys.KEY_TRANSLATE_MESSAGE) + assertEquals("KEY_BASE_URL", BundleKeys.KEY_BASE_URL) + assertEquals("KEY_IS_ACCOUNT_IMPORT", BundleKeys.KEY_IS_ACCOUNT_IMPORT) + assertEquals("KEY_ORIGINAL_PROTOCOL", BundleKeys.KEY_ORIGINAL_PROTOCOL) + assertEquals("KEY_OPERATION_CODE", BundleKeys.KEY_OPERATION_CODE) + assertEquals("KEY_APP_ITEM_PACKAGE_NAME", BundleKeys.KEY_APP_ITEM_PACKAGE_NAME) + assertEquals("KEY_APP_ITEM_NAME", BundleKeys.KEY_APP_ITEM_NAME) + assertEquals("KEY_CONVERSATION_PASSWORD", BundleKeys.KEY_CONVERSATION_PASSWORD) + assertEquals("KEY_ROOM_TOKEN", BundleKeys.KEY_ROOM_TOKEN) + assertEquals("KEY_ROOM_ONE_TO_ONE", BundleKeys.KEY_ROOM_ONE_TO_ONE) + assertEquals("KEY_NEW_CONVERSATION", BundleKeys.KEY_NEW_CONVERSATION) + assertEquals("KEY_ADD_PARTICIPANTS", BundleKeys.KEY_ADD_PARTICIPANTS) + assertEquals("KEY_EXISTING_PARTICIPANTS", BundleKeys.KEY_EXISTING_PARTICIPANTS) + assertEquals("KEY_CALL_URL", BundleKeys.KEY_CALL_URL) + assertEquals("KEY_NEW_ROOM_NAME", BundleKeys.KEY_NEW_ROOM_NAME) + assertEquals("KEY_MODIFIED_BASE_URL", BundleKeys.KEY_MODIFIED_BASE_URL) + assertEquals("KEY_NOTIFICATION_SUBJECT", BundleKeys.KEY_NOTIFICATION_SUBJECT) + assertEquals("KEY_NOTIFICATION_SIGNATURE", BundleKeys.KEY_NOTIFICATION_SIGNATURE) + assertEquals("KEY_INTERNAL_USER_ID", BundleKeys.KEY_INTERNAL_USER_ID) + assertEquals("KEY_CONVERSATION_TYPE", BundleKeys.KEY_CONVERSATION_TYPE) + assertEquals("KEY_INVITED_PARTICIPANTS", BundleKeys.KEY_INVITED_PARTICIPANTS) + assertEquals("KEY_INVITED_CIRCLE", BundleKeys.KEY_INVITED_CIRCLE) + assertEquals("KEY_INVITED_GROUP", BundleKeys.KEY_INVITED_GROUP) + assertEquals("KEY_INVITED_EMAIL", BundleKeys.KEY_INVITED_EMAIL) + } + + @Test + fun testBundleKeysValues2() { + assertEquals("KEY_CONVERSATION_NAME", BundleKeys.KEY_CONVERSATION_NAME) + assertEquals("KEY_RECORDING_STATE", BundleKeys.KEY_RECORDING_STATE) + assertEquals("KEY_CALL_VOICE_ONLY", BundleKeys.KEY_CALL_VOICE_ONLY) + assertEquals("KEY_CALL_WITHOUT_NOTIFICATION", BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION) + assertEquals("KEY_FROM_NOTIFICATION_START_CALL", BundleKeys.KEY_FROM_NOTIFICATION_START_CALL) + assertEquals("KEY_ROOM_ID", BundleKeys.KEY_ROOM_ID) + assertEquals("KEY_ARE_CALL_SOUNDS", BundleKeys.KEY_ARE_CALL_SOUNDS) + assertEquals("KEY_FILE_PATHS", BundleKeys.KEY_FILE_PATHS) + assertEquals("KEY_ACCOUNT", BundleKeys.KEY_ACCOUNT) + assertEquals("KEY_FILE_ID", BundleKeys.KEY_FILE_ID) + assertEquals("KEY_NOTIFICATION_ID", BundleKeys.KEY_NOTIFICATION_ID) + assertEquals("KEY_NOTIFICATION_TIMESTAMP", BundleKeys.KEY_NOTIFICATION_TIMESTAMP) + assertEquals("KEY_SHARED_TEXT", BundleKeys.KEY_SHARED_TEXT) + assertEquals("KEY_GEOCODING_QUERY", BundleKeys.KEY_GEOCODING_QUERY) + assertEquals("KEY_META_DATA", BundleKeys.KEY_META_DATA) + assertEquals("KEY_FORWARD_MSG_FLAG", BundleKeys.KEY_FORWARD_MSG_FLAG) + assertEquals("KEY_FORWARD_MSG_TEXT", BundleKeys.KEY_FORWARD_MSG_TEXT) + assertEquals("KEY_FORWARD_HIDE_SOURCE_ROOM", BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM) + assertEquals("KEY_SYSTEM_NOTIFICATION_ID", BundleKeys.KEY_SYSTEM_NOTIFICATION_ID) + assertEquals("KEY_MESSAGE_ID", BundleKeys.KEY_MESSAGE_ID) + assertEquals("KEY_MIME_TYPE_FILTER", BundleKeys.KEY_MIME_TYPE_FILTER) + assertEquals( + "KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO", + BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO + ) + assertEquals( + "KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO", + BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO + ) + assertEquals("KEY_IS_MODERATOR", BundleKeys.KEY_IS_MODERATOR) + assertEquals("KEY_SWITCH_TO_ROOM", BundleKeys.KEY_SWITCH_TO_ROOM) + assertEquals("KEY_START_CALL_AFTER_ROOM_SWITCH", BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH) + assertEquals("KEY_IS_BREAKOUT_ROOM", BundleKeys.KEY_IS_BREAKOUT_ROOM) + assertEquals("KEY_NOTIFICATION_RESTRICT_DELETION", BundleKeys.KEY_NOTIFICATION_RESTRICT_DELETION) + assertEquals("KEY_DISMISS_RECORDING_URL", BundleKeys.KEY_DISMISS_RECORDING_URL) + assertEquals("KEY_SHARE_RECORDING_TO_CHAT_URL", BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL) + assertEquals("KEY_GEOCODING_RESULT", BundleKeys.KEY_GEOCODING_RESULT) + assertEquals("ADD_ADDITIONAL_ACCOUNT", BundleKeys.ADD_ADDITIONAL_ACCOUNT) + assertEquals("SAVED_TRANSLATED_MESSAGE", BundleKeys.SAVED_TRANSLATED_MESSAGE) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/utils/DoNotDisturbUtilsTest.java b/app/src/test/java/com/nextcloud/talk/utils/DoNotDisturbUtilsTest.java new file mode 100644 index 0000000..b1e7a2e --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/utils/DoNotDisturbUtilsTest.java @@ -0,0 +1,60 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils; + +import android.app.NotificationManager; +import android.content.Context; +import android.media.AudioManager; +import android.os.Build; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; + +public class DoNotDisturbUtilsTest { + + @Mock + private Context context; + + @Mock + private NotificationManager notificationManager; + + @Mock + private AudioManager audioManager; + + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + when(context.getSystemService(Context.NOTIFICATION_SERVICE)).thenReturn(notificationManager); + when(context.getSystemService(Context.AUDIO_SERVICE)).thenReturn(audioManager); + } + + @Test + public void shouldPlaySound_givenAndroidMAndInterruptionFilterNone_assertReturnsFalse() { + DoNotDisturbUtils.INSTANCE.setTestingBuildVersion(Build.VERSION_CODES.M); + + when(notificationManager.getCurrentInterruptionFilter()).thenReturn(NotificationManager.INTERRUPTION_FILTER_NONE); + when(audioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_NORMAL); + + assertFalse("shouldPlaySound incorrectly returned true", + DoNotDisturbUtils.INSTANCE.shouldPlaySound(context)); + } + + @Test + public void shouldPlaySound_givenRingerModeNotNormal_assertReturnsFalse() throws Exception { + DoNotDisturbUtils.INSTANCE.setTestingBuildVersion(Build.VERSION_CODES.LOLLIPOP); + when(audioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); + + assertFalse("shouldPlaySound incorrectly returned true", + DoNotDisturbUtils.INSTANCE.shouldPlaySound(context)); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt b/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt new file mode 100644 index 0000000..76b08b6 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt @@ -0,0 +1,97 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-FileCopyrightText: 2022 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.domain.ConversationModel +import com.nextcloud.talk.models.json.capabilities.SpreedCapability +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.ConversationEnums +import com.nextcloud.talk.models.json.participants.Participant +import junit.framework.TestCase +import org.junit.Test + +class ParticipantPermissionsTest : TestCase() { + + @Test + fun test_areFlagsSet() { + val spreedCapability = SpreedCapability() + val conversation = createConversation() + + conversation.permissions = ParticipantPermissions.PUBLISH_SCREEN or + ParticipantPermissions.JOIN_CALL or + ParticipantPermissions.DEFAULT + + val user = User() + user.id = 1 + + val attendeePermissions = + ParticipantPermissions( + spreedCapability, + ConversationModel.mapToConversationModel(conversation, user) + ) + + assert(attendeePermissions.canPublishScreen) + assert(attendeePermissions.canJoinCall) + assert(attendeePermissions.isDefault) + + assertFalse(attendeePermissions.isCustom) + assertFalse(attendeePermissions.canStartCall()) + assertFalse(attendeePermissions.canIgnoreLobby()) + assertTrue(attendeePermissions.canPublishAudio()) + assertTrue(attendeePermissions.canPublishVideo()) + } + + private fun createConversation() = + Conversation( + token = "test", + name = "test", + displayName = "test", + description = "test", + type = ConversationEnums.ConversationType.DUMMY, + lastPing = 1, + participantType = Participant.ParticipantType.DUMMY, + hasPassword = true, + sessionId = "test", + actorId = "test", + actorType = "test", + password = "test", + favorite = false, + lastActivity = 1, + unreadMessages = 1, + unreadMention = false, + lastMessage = null, + objectType = ConversationEnums.ObjectType.DEFAULT, + notificationLevel = ConversationEnums.NotificationLevel.ALWAYS, + conversationReadOnlyState = ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_WRITE, + lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS, + lobbyTimer = 1, + lastReadMessage = 1, + lastCommonReadMessage = 1, + hasCall = true, + callFlag = 1, + canStartCall = false, + canLeaveConversation = true, + canDeleteConversation = true, + unreadMentionDirect = true, + notificationCalls = 1, + permissions = 1, + messageExpiration = 1, + status = "test", + statusIcon = "test", + statusMessage = "test", + statusClearAt = 1, + callRecording = 1, + avatarVersion = "test", + hasCustomAvatar = true, + callStartTime = 1, + recordingConsentRequired = 1, + remoteServer = "", + remoteToken = "" + ) +} diff --git a/app/src/test/java/com/nextcloud/talk/utils/UserIdUtilsTest.kt b/app/src/test/java/com/nextcloud/talk/utils/UserIdUtilsTest.kt new file mode 100644 index 0000000..eb5237a --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/utils/UserIdUtilsTest.kt @@ -0,0 +1,42 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Samanwith KSN + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import com.nextcloud.talk.data.user.model.User +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +class UserIdUtilsTest { + + @Mock + private lateinit var user: User + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + } + + @Test + fun testGetIdForUser_if_userIsNull_returnsNoId() { + Mockito.`when`(user.id).thenReturn(null) + val result = UserIdUtils.getIdForUser(user) + Assert.assertEquals("The id is NO_ID when user is null", UserIdUtils.NO_ID, result) + } + + @Test + fun testGetIdForUser_if_userIdIsSet_returnsUserId() { + val expectedId: Long = 12345 + Mockito.`when`(user.id).thenReturn(expectedId) + val result = UserIdUtils.getIdForUser(user) + Assert.assertEquals("The id is correct user id is not null", expectedId, result) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/viewmodels/AbstractViewModelTest.kt b/app/src/test/java/com/nextcloud/talk/viewmodels/AbstractViewModelTest.kt new file mode 100644 index 0000000..c4b5e8c --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/viewmodels/AbstractViewModelTest.kt @@ -0,0 +1,40 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2023 Álvaro Brey + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.viewmodels + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import org.junit.BeforeClass +import org.junit.Rule + +open class AbstractViewModelTest { + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + companion object { + @JvmStatic + @BeforeClass + fun setUpClass() { + RxJavaPlugins.setIoSchedulerHandler { + Schedulers.trampoline() + } + RxJavaPlugins.setComputationSchedulerHandler { + Schedulers.trampoline() + } + RxJavaPlugins.setNewThreadSchedulerHandler { + Schedulers.trampoline() + } + + RxAndroidPlugins.setInitMainThreadSchedulerHandler { + Schedulers.trampoline() + } + } + } +} diff --git a/app/src/test/java/com/nextcloud/talk/viewmodels/CallRecordingViewModelTest.kt b/app/src/test/java/com/nextcloud/talk/viewmodels/CallRecordingViewModelTest.kt new file mode 100644 index 0000000..0e4dfc0 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/viewmodels/CallRecordingViewModelTest.kt @@ -0,0 +1,114 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.viewmodels + +import com.nextcloud.talk.test.fakes.FakeCallRecordingRepository +import com.vividsolutions.jts.util.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.MockitoAnnotations + +class CallRecordingViewModelTest : AbstractViewModelTest() { + + private val repository = FakeCallRecordingRepository() + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + } + + @Test + fun testCallRecordingViewModel_clickStartRecord() { + val viewModel = CallRecordingViewModel(repository) + viewModel.setData("foo") + viewModel.clickRecordButton() + + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStartingState) + + // fake to execute setRecordingState which would be triggered by signaling message + viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTED_VIDEO_CODE) + + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStartedState) + } + + @Test + fun testCallRecordingViewModel_clickStopRecord() { + val viewModel = CallRecordingViewModel(repository) + viewModel.setData("foo") + viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTED_VIDEO_CODE) + + Assert.equals(true, (viewModel.viewState.value as CallRecordingViewModel.RecordingStartedState).showStartedInfo) + + viewModel.clickRecordButton() + + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingConfirmStopState) + + viewModel.stopRecording() + + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStoppedState) + } + + @Test + fun testCallRecordingViewModel_keepConfirmState() { + val viewModel = CallRecordingViewModel(repository) + viewModel.setData("foo") + viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTED_VIDEO_CODE) + + Assert.equals(true, (viewModel.viewState.value as CallRecordingViewModel.RecordingStartedState).showStartedInfo) + + viewModel.clickRecordButton() + + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingConfirmStopState) + + viewModel.clickRecordButton() + + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingConfirmStopState) + } + + @Test + fun testCallRecordingViewModel_continueRecordingWhenDismissStopDialog() { + val viewModel = CallRecordingViewModel(repository) + viewModel.setData("foo") + viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTED_VIDEO_CODE) + viewModel.clickRecordButton() + + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingConfirmStopState) + + viewModel.dismissStopRecording() + + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStartedState) + + Assert.equals( + false, + (viewModel.viewState.value as CallRecordingViewModel.RecordingStartedState).showStartedInfo + ) + } + + @Test + fun testSetRecordingStateDirectly() { + val viewModel = CallRecordingViewModel(repository) + viewModel.setData("foo") + + viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STOPPED_CODE) + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStoppedState) + + viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTED_AUDIO_CODE) + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStartedState) + + viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTED_VIDEO_CODE) + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStartedState) + + viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTING_AUDIO_CODE) + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStartingState) + + viewModel.setRecordingState(CallRecordingViewModel.RECORDING_STARTING_VIDEO_CODE) + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingStartingState) + + viewModel.setRecordingState(CallRecordingViewModel.RECORDING_FAILED_CODE) + Assert.isTrue(viewModel.viewState.value is CallRecordingViewModel.RecordingErrorState) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/webrtc/DataChannelMessageNotifierTest.kt b/app/src/test/java/com/nextcloud/talk/webrtc/DataChannelMessageNotifierTest.kt new file mode 100644 index 0000000..ead358b --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/webrtc/DataChannelMessageNotifierTest.kt @@ -0,0 +1,75 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Samanwith KSN + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify + +class DataChannelMessageNotifierTest { + + private lateinit var notifier: DataChannelMessageNotifier + private lateinit var listener: PeerConnectionWrapper.DataChannelMessageListener + + @Before + fun setUp() { + notifier = DataChannelMessageNotifier() + listener = mock(PeerConnectionWrapper.DataChannelMessageListener::class.java) + } + + @Test + fun testAddListener() { + notifier.addListener(listener) + assertTrue(notifier.dataChannelMessageListeners.contains(listener)) + } + + @Test + fun testRemoveListener() { + notifier.addListener(listener) + notifier.removeListener(listener) + assertFalse(notifier.dataChannelMessageListeners.contains(listener)) + } + + @Test + fun testNotifyAudioOn() { + notifier.addListener(listener) + notifier.notifyAudioOn() + verify(listener).onAudioOn() + } + + @Test + fun testNotifyAudioOff() { + notifier.addListener(listener) + notifier.notifyAudioOff() + verify(listener).onAudioOff() + } + + @Test + fun testNotifyVideoOn() { + notifier.addListener(listener) + notifier.notifyVideoOn() + verify(listener).onVideoOn() + } + + @Test + fun testNotifyVideoOff() { + notifier.addListener(listener) + notifier.notifyVideoOff() + verify(listener).onVideoOff() + } + + @Test + fun testNotifyNickChanged() { + notifier.addListener(listener) + val newNick = "NewNick" + notifier.notifyNickChanged(newNick) + verify(listener).onNickChanged(newNick) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/webrtc/GlobalsTest.kt b/app/src/test/java/com/nextcloud/talk/webrtc/GlobalsTest.kt new file mode 100644 index 0000000..6ceb0d5 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/webrtc/GlobalsTest.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Samanwith KSN + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc + +import org.junit.Assert +import org.junit.Test + +class GlobalsTest { + @Test + fun testRoomToken() { + Assert.assertEquals("roomToken", Globals.ROOM_TOKEN) + } + + @Test + fun testTargetParticipants() { + Assert.assertEquals("participants", Globals.TARGET_PARTICIPANTS) + } + + @Test + fun testTargetRoom() { + Assert.assertEquals("room", Globals.TARGET_ROOM) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/webrtc/PeerConnectionNotifierTest.kt b/app/src/test/java/com/nextcloud/talk/webrtc/PeerConnectionNotifierTest.kt new file mode 100644 index 0000000..e396b88 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/webrtc/PeerConnectionNotifierTest.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2023 Samanwith KSN + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc + +import com.nextcloud.talk.webrtc.PeerConnectionWrapper.PeerConnectionObserver +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.webrtc.MediaStream +import org.webrtc.PeerConnection + +class PeerConnectionNotifierTest { + private var notifier: PeerConnectionNotifier? = null + private var observer1: PeerConnectionObserver? = null + private var observer2: PeerConnectionObserver? = null + private var mockStream: MediaStream? = null + + @Before + fun setUp() { + notifier = PeerConnectionNotifier() + observer1 = Mockito.mock(PeerConnectionObserver::class.java) + observer2 = Mockito.mock(PeerConnectionObserver::class.java) + mockStream = Mockito.mock(MediaStream::class.java) + } + + @Test + fun testAddObserver() { + notifier!!.addObserver(observer1) + notifier!!.notifyStreamAdded(mockStream) + Mockito.verify(observer1)?.onStreamAdded(mockStream) + Mockito.verify(observer2, Mockito.never())?.onStreamAdded(mockStream) + } + + @Test + fun testRemoveObserver() { + notifier!!.addObserver(observer1) + notifier!!.addObserver(observer2) + notifier!!.removeObserver(observer1) + notifier!!.notifyStreamAdded(mockStream) + Mockito.verify(observer1, Mockito.never())?.onStreamAdded(mockStream) + Mockito.verify(observer2)?.onStreamAdded(mockStream) + } + + @Test + fun testNotifyStreamAdded() { + notifier!!.addObserver(observer1) + notifier!!.notifyStreamAdded(mockStream) + Mockito.verify(observer1)?.onStreamAdded(mockStream) + } + + @Test + fun testNotifyStreamRemoved() { + notifier!!.addObserver(observer1) + notifier!!.notifyStreamRemoved(mockStream) + Mockito.verify(observer1)?.onStreamRemoved(mockStream) + } + + @Test + fun testNotifyIceConnectionStateChanged() { + notifier!!.addObserver(observer1) + notifier!!.notifyIceConnectionStateChanged(PeerConnection.IceConnectionState.CONNECTED) + Mockito.verify(observer1)?.onIceConnectionStateChanged(PeerConnection.IceConnectionState.CONNECTED) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/webrtc/PeerConnectionWrapperTest.kt b/app/src/test/java/com/nextcloud/talk/webrtc/PeerConnectionWrapperTest.kt new file mode 100644 index 0000000..dd0e74b --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/webrtc/PeerConnectionWrapperTest.kt @@ -0,0 +1,760 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.webrtc + +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.signaling.SignalingMessageReceiver +import com.nextcloud.talk.signaling.SignalingMessageSender +import com.nextcloud.talk.webrtc.PeerConnectionWrapper.DataChannelMessageListener +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatcher +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.argThat +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito +import org.mockito.Mockito.atLeast +import org.mockito.Mockito.atMostOnce +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.never +import org.mockito.invocation.InvocationOnMock +import org.mockito.stubbing.Answer +import org.webrtc.DataChannel +import org.webrtc.MediaConstraints +import org.webrtc.PeerConnection +import org.webrtc.PeerConnectionFactory +import java.nio.ByteBuffer +import java.util.HashMap +import kotlin.concurrent.thread + +@Suppress("LongMethod", "TooGenericExceptionCaught") +class PeerConnectionWrapperTest { + + private var peerConnectionWrapper: PeerConnectionWrapper? = null + private var mockedPeerConnection: PeerConnection? = null + private var mockedPeerConnectionFactory: PeerConnectionFactory? = null + private var mockedSignalingMessageReceiver: SignalingMessageReceiver? = null + private var mockedSignalingMessageSender: SignalingMessageSender? = null + + /** + * Helper answer for DataChannel methods. + */ + private class ReturnValueOrThrowIfDisposed(val value: T) : Answer { + override fun answer(currentInvocation: InvocationOnMock): T { + if (Mockito.mockingDetails(currentInvocation.mock).invocations.find { + it!!.method.name === "dispose" + } !== null + ) { + throw IllegalStateException("DataChannel has been disposed") + } + + return value + } + } + + /** + * Helper matcher for DataChannelMessages. + */ + private inner class MatchesDataChannelMessage(private val expectedDataChannelMessage: DataChannelMessage) : + ArgumentMatcher { + override fun matches(buffer: DataChannel.Buffer): Boolean { + // DataChannel.Buffer does not implement "equals", so the comparison needs to be done on the ByteBuffer + // instead. + return dataChannelMessageToBuffer(expectedDataChannelMessage).data.equals(buffer.data) + } + } + + private fun dataChannelMessageToBuffer(dataChannelMessage: DataChannelMessage) = + DataChannel.Buffer( + ByteBuffer.wrap(LoganSquare.serialize(dataChannelMessage).toByteArray()), + false + ) + + @Before + fun setUp() { + mockedPeerConnection = Mockito.mock(PeerConnection::class.java) + mockedPeerConnectionFactory = Mockito.mock(PeerConnectionFactory::class.java) + mockedSignalingMessageReceiver = Mockito.mock(SignalingMessageReceiver::class.java) + mockedSignalingMessageSender = Mockito.mock(SignalingMessageSender::class.java) + } + + @Test + fun testSendDataChannelMessage() { + Mockito.`when`( + mockedPeerConnectionFactory!!.createPeerConnection( + any(PeerConnection.RTCConfiguration::class.java), + any(PeerConnection.Observer::class.java) + ) + ).thenReturn(mockedPeerConnection) + + val mockedStatusDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedStatusDataChannel.label()).thenReturn("status") + Mockito.`when`(mockedStatusDataChannel.state()).thenReturn(DataChannel.State.OPEN) + Mockito.`when`(mockedPeerConnection!!.createDataChannel(eq("status"), any())) + .thenReturn(mockedStatusDataChannel) + + peerConnectionWrapper = PeerConnectionWrapper( + mockedPeerConnectionFactory, + ArrayList(), + MediaConstraints(), + "the-session-id", + "the-local-session-id", + null, + true, + true, + "video", + mockedSignalingMessageReceiver, + mockedSignalingMessageSender + ) + + peerConnectionWrapper!!.send(DataChannelMessage("the-message-type")) + + Mockito.verify(mockedStatusDataChannel).send( + argThat(MatchesDataChannelMessage(DataChannelMessage("the-message-type"))) + ) + } + + @Test + fun testSendDataChannelMessageWithOpenRemoteDataChannel() { + val peerConnectionObserverArgumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(PeerConnection.Observer::class.java) + + Mockito.`when`( + mockedPeerConnectionFactory!!.createPeerConnection( + any(PeerConnection.RTCConfiguration::class.java), + peerConnectionObserverArgumentCaptor.capture() + ) + ).thenReturn(mockedPeerConnection) + + val mockedStatusDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedStatusDataChannel.label()).thenReturn("status") + Mockito.`when`(mockedStatusDataChannel.state()).thenReturn(DataChannel.State.OPEN) + Mockito.`when`(mockedPeerConnection!!.createDataChannel(eq("status"), any())) + .thenReturn(mockedStatusDataChannel) + + peerConnectionWrapper = PeerConnectionWrapper( + mockedPeerConnectionFactory, + ArrayList(), + MediaConstraints(), + "the-session-id", + "the-local-session-id", + null, + true, + true, + "video", + mockedSignalingMessageReceiver, + mockedSignalingMessageSender + ) + + val mockedRandomIdDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedRandomIdDataChannel.label()).thenReturn("random-id") + Mockito.`when`(mockedRandomIdDataChannel.state()).thenReturn(DataChannel.State.OPEN) + peerConnectionObserverArgumentCaptor.value.onDataChannel(mockedRandomIdDataChannel) + + peerConnectionWrapper!!.send(DataChannelMessage("the-message-type")) + + Mockito.verify(mockedStatusDataChannel).send( + argThat(MatchesDataChannelMessage(DataChannelMessage("the-message-type"))) + ) + Mockito.verify(mockedRandomIdDataChannel, never()).send(any()) + } + + @Test + fun testSendDataChannelMessageBeforeOpeningDataChannel() { + Mockito.`when`( + mockedPeerConnectionFactory!!.createPeerConnection( + any(PeerConnection.RTCConfiguration::class.java), + any(PeerConnection.Observer::class.java) + ) + ).thenReturn(mockedPeerConnection) + + val mockedStatusDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedStatusDataChannel.label()).thenReturn("status") + Mockito.`when`(mockedStatusDataChannel.state()).thenReturn(DataChannel.State.CONNECTING) + Mockito.`when`(mockedPeerConnection!!.createDataChannel(eq("status"), any())) + .thenReturn(mockedStatusDataChannel) + + val statusDataChannelObserverArgumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(DataChannel.Observer::class.java) + + doNothing().`when`(mockedStatusDataChannel).registerObserver(statusDataChannelObserverArgumentCaptor.capture()) + + peerConnectionWrapper = PeerConnectionWrapper( + mockedPeerConnectionFactory, + ArrayList(), + MediaConstraints(), + "the-session-id", + "the-local-session-id", + null, + true, + true, + "video", + mockedSignalingMessageReceiver, + mockedSignalingMessageSender + ) + + peerConnectionWrapper!!.send(DataChannelMessage("the-message-type")) + peerConnectionWrapper!!.send(DataChannelMessage("another-message-type")) + + Mockito.verify(mockedStatusDataChannel, never()).send(any()) + + Mockito.`when`(mockedStatusDataChannel.state()).thenReturn(DataChannel.State.OPEN) + statusDataChannelObserverArgumentCaptor.value.onStateChange() + + Mockito.verify(mockedStatusDataChannel).send( + argThat(MatchesDataChannelMessage(DataChannelMessage("the-message-type"))) + ) + Mockito.verify(mockedStatusDataChannel).send( + argThat(MatchesDataChannelMessage(DataChannelMessage("another-message-type"))) + ) + } + + @Test + fun testSendDataChannelMessageBeforeOpeningDataChannelWithDifferentThreads() { + // A brute force approach is used to test race conditions between different threads just repeating the test + // several times. Due to this the test passing could be a false positive, as it could have been a matter of + // luck, but even if the test may wrongly pass sometimes it is better than nothing (although, in general, with + // that number of reruns, it fails when it should). + for (i in 1..1000) { + Mockito.`when`( + mockedPeerConnectionFactory!!.createPeerConnection( + any(PeerConnection.RTCConfiguration::class.java), + any(PeerConnection.Observer::class.java) + ) + ).thenReturn(mockedPeerConnection) + + val mockedStatusDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedStatusDataChannel.label()).thenReturn("status") + Mockito.`when`(mockedStatusDataChannel.state()).thenReturn(DataChannel.State.CONNECTING) + Mockito.`when`(mockedPeerConnection!!.createDataChannel(eq("status"), any())) + .thenReturn(mockedStatusDataChannel) + + val statusDataChannelObserverArgumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(DataChannel.Observer::class.java) + + doNothing().`when`(mockedStatusDataChannel) + .registerObserver(statusDataChannelObserverArgumentCaptor.capture()) + + peerConnectionWrapper = PeerConnectionWrapper( + mockedPeerConnectionFactory, + ArrayList(), + MediaConstraints(), + "the-session-id", + "the-local-session-id", + null, + true, + true, + "video", + mockedSignalingMessageReceiver, + mockedSignalingMessageSender + ) + + val dataChannelMessageCount = 5 + + val sendThread = thread { + for (j in 1..dataChannelMessageCount) { + peerConnectionWrapper!!.send(DataChannelMessage("the-message-type-$j")) + } + } + + // Exceptions thrown in threads are not propagated to the main thread, so it needs to be explicitly done + // (for example, for ConcurrentModificationExceptions when iterating over the data channel messages). + var exceptionOnStateChange: Exception? = null + + val openDataChannelThread = thread { + Mockito.`when`(mockedStatusDataChannel.state()).thenReturn(DataChannel.State.OPEN) + + try { + statusDataChannelObserverArgumentCaptor.value.onStateChange() + } catch (e: Exception) { + exceptionOnStateChange = e + } + } + + sendThread.join() + openDataChannelThread.join() + + if (exceptionOnStateChange !== null) { + throw exceptionOnStateChange!! + } + + val inOrder = inOrder(mockedStatusDataChannel) + + for (j in 1..dataChannelMessageCount) { + inOrder.verify(mockedStatusDataChannel).send( + argThat(MatchesDataChannelMessage(DataChannelMessage("the-message-type-$j"))) + ) + } + } + } + + @Test + fun testReceiveDataChannelMessage() { + Mockito.`when`( + mockedPeerConnectionFactory!!.createPeerConnection( + any(PeerConnection.RTCConfiguration::class.java), + any(PeerConnection.Observer::class.java) + ) + ).thenReturn(mockedPeerConnection) + + val mockedStatusDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedStatusDataChannel.label()).thenReturn("status") + Mockito.`when`(mockedStatusDataChannel.state()).thenReturn(DataChannel.State.OPEN) + Mockito.`when`(mockedPeerConnection!!.createDataChannel(eq("status"), any())) + .thenReturn(mockedStatusDataChannel) + + val statusDataChannelObserverArgumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(DataChannel.Observer::class.java) + + doNothing().`when`(mockedStatusDataChannel).registerObserver(statusDataChannelObserverArgumentCaptor.capture()) + + peerConnectionWrapper = PeerConnectionWrapper( + mockedPeerConnectionFactory, + ArrayList(), + MediaConstraints(), + "the-session-id", + "the-local-session-id", + null, + true, + true, + "video", + mockedSignalingMessageReceiver, + mockedSignalingMessageSender + ) + + val mockedDataChannelMessageListener = Mockito.mock(DataChannelMessageListener::class.java) + peerConnectionWrapper!!.addListener(mockedDataChannelMessageListener) + + // The payload must be a map to be able to serialize it and, therefore, generate the data that would have been + // received from another participant, so it is not possible to test receiving the nick as a String payload. + val payloadMap = HashMap() + payloadMap["name"] = "the-nick-in-map" + + statusDataChannelObserverArgumentCaptor.value.onMessage( + dataChannelMessageToBuffer(DataChannelMessage("nickChanged", null, payloadMap)) + ) + + Mockito.verify(mockedDataChannelMessageListener).onNickChanged("the-nick-in-map") + Mockito.verifyNoMoreInteractions(mockedDataChannelMessageListener) + + statusDataChannelObserverArgumentCaptor.value.onMessage( + dataChannelMessageToBuffer(DataChannelMessage("audioOn")) + ) + + Mockito.verify(mockedDataChannelMessageListener).onAudioOn() + Mockito.verifyNoMoreInteractions(mockedDataChannelMessageListener) + + statusDataChannelObserverArgumentCaptor.value.onMessage( + dataChannelMessageToBuffer(DataChannelMessage("audioOff")) + ) + + Mockito.verify(mockedDataChannelMessageListener).onAudioOff() + Mockito.verifyNoMoreInteractions(mockedDataChannelMessageListener) + + statusDataChannelObserverArgumentCaptor.value.onMessage( + dataChannelMessageToBuffer(DataChannelMessage("videoOn")) + ) + + Mockito.verify(mockedDataChannelMessageListener).onVideoOn() + Mockito.verifyNoMoreInteractions(mockedDataChannelMessageListener) + + statusDataChannelObserverArgumentCaptor.value.onMessage( + dataChannelMessageToBuffer(DataChannelMessage("videoOff")) + ) + + Mockito.verify(mockedDataChannelMessageListener).onVideoOff() + Mockito.verifyNoMoreInteractions(mockedDataChannelMessageListener) + } + + @Test + fun testReceiveDataChannelMessageWithOpenRemoteDataChannel() { + val peerConnectionObserverArgumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(PeerConnection.Observer::class.java) + + Mockito.`when`( + mockedPeerConnectionFactory!!.createPeerConnection( + any(PeerConnection.RTCConfiguration::class.java), + peerConnectionObserverArgumentCaptor.capture() + ) + ).thenReturn(mockedPeerConnection) + + val mockedStatusDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedStatusDataChannel.label()).thenReturn("status") + Mockito.`when`(mockedStatusDataChannel.state()).thenReturn(DataChannel.State.OPEN) + Mockito.`when`(mockedPeerConnection!!.createDataChannel(eq("status"), any())) + .thenReturn(mockedStatusDataChannel) + + val statusDataChannelObserverArgumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(DataChannel.Observer::class.java) + + doNothing().`when`(mockedStatusDataChannel).registerObserver(statusDataChannelObserverArgumentCaptor.capture()) + + peerConnectionWrapper = PeerConnectionWrapper( + mockedPeerConnectionFactory, + ArrayList(), + MediaConstraints(), + "the-session-id", + "the-local-session-id", + null, + true, + true, + "video", + mockedSignalingMessageReceiver, + mockedSignalingMessageSender + ) + + val randomIdDataChannelObserverArgumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(DataChannel.Observer::class.java) + + val mockedRandomIdDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedRandomIdDataChannel.label()).thenReturn("random-id") + Mockito.`when`(mockedRandomIdDataChannel.state()).thenReturn(DataChannel.State.OPEN) + doNothing().`when`(mockedRandomIdDataChannel).registerObserver( + randomIdDataChannelObserverArgumentCaptor.capture() + ) + peerConnectionObserverArgumentCaptor.value.onDataChannel(mockedRandomIdDataChannel) + + val mockedDataChannelMessageListener = Mockito.mock(DataChannelMessageListener::class.java) + peerConnectionWrapper!!.addListener(mockedDataChannelMessageListener) + + statusDataChannelObserverArgumentCaptor.value.onMessage( + dataChannelMessageToBuffer(DataChannelMessage("audioOn")) + ) + + Mockito.verify(mockedDataChannelMessageListener).onAudioOn() + Mockito.verifyNoMoreInteractions(mockedDataChannelMessageListener) + + randomIdDataChannelObserverArgumentCaptor.value.onMessage( + dataChannelMessageToBuffer(DataChannelMessage("audioOff")) + ) + + Mockito.verify(mockedDataChannelMessageListener).onAudioOff() + Mockito.verifyNoMoreInteractions(mockedDataChannelMessageListener) + } + + @Test + fun testRemovePeerConnectionWithOpenRemoteDataChannel() { + val peerConnectionObserverArgumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(PeerConnection.Observer::class.java) + + Mockito.`when`( + mockedPeerConnectionFactory!!.createPeerConnection( + any(PeerConnection.RTCConfiguration::class.java), + peerConnectionObserverArgumentCaptor.capture() + ) + ).thenReturn(mockedPeerConnection) + + val mockedStatusDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedStatusDataChannel.label()).thenReturn("status") + Mockito.`when`(mockedStatusDataChannel.state()).thenReturn(DataChannel.State.OPEN) + Mockito.`when`(mockedPeerConnection!!.createDataChannel(eq("status"), any())) + .thenReturn(mockedStatusDataChannel) + + peerConnectionWrapper = PeerConnectionWrapper( + mockedPeerConnectionFactory, + ArrayList(), + MediaConstraints(), + "the-session-id", + "the-local-session-id", + null, + true, + true, + "video", + mockedSignalingMessageReceiver, + mockedSignalingMessageSender + ) + + val mockedRandomIdDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedRandomIdDataChannel.label()).thenReturn("random-id") + Mockito.`when`(mockedRandomIdDataChannel.state()).thenReturn(DataChannel.State.OPEN) + peerConnectionObserverArgumentCaptor.value.onDataChannel(mockedRandomIdDataChannel) + + peerConnectionWrapper!!.removePeerConnection() + + Mockito.verify(mockedStatusDataChannel).dispose() + Mockito.verify(mockedRandomIdDataChannel).dispose() + } + + @Test + fun testRemovePeerConnectionWhileAddingRemoteDataChannelsWithDifferentThreads() { + // A brute force approach is used to test race conditions between different threads just repeating the test + // several times. Due to this the test passing could be a false positive, as it could have been a matter of + // luck, but even if the test may wrongly pass sometimes it is better than nothing (although, in general, with + // that number of reruns, it fails when it should). + for (i in 1..1000) { + val peerConnectionObserverArgumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(PeerConnection.Observer::class.java) + + Mockito.`when`( + mockedPeerConnectionFactory!!.createPeerConnection( + any(PeerConnection.RTCConfiguration::class.java), + peerConnectionObserverArgumentCaptor.capture() + ) + ).thenReturn(mockedPeerConnection) + + val mockedStatusDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedStatusDataChannel.label()).thenAnswer(ReturnValueOrThrowIfDisposed("status")) + Mockito.`when`(mockedStatusDataChannel.state()).thenAnswer( + ReturnValueOrThrowIfDisposed(DataChannel.State.OPEN) + ) + Mockito.`when`(mockedPeerConnection!!.createDataChannel(eq("status"), any())) + .thenReturn(mockedStatusDataChannel) + + peerConnectionWrapper = PeerConnectionWrapper( + mockedPeerConnectionFactory, + ArrayList(), + MediaConstraints(), + "the-session-id", + "the-local-session-id", + null, + true, + true, + "video", + mockedSignalingMessageReceiver, + mockedSignalingMessageSender + ) + + val dataChannelCount = 5 + + val mockedRandomIdDataChannels: MutableList = ArrayList() + val dataChannelObservers: MutableList = ArrayList() + for (j in 0.. + if (Mockito.mockingDetails(invocation.mock).invocations.find { + it!!.method.name === "dispose" + } !== null + ) { + throw IllegalStateException("DataChannel has been disposed") + } + + dataChannelObservers[j] = invocation.getArgument(0, DataChannel.Observer::class.java) + + null + }.`when`(mockedRandomIdDataChannels[j]).registerObserver(any()) + } + + val onDataChannelThread = thread { + // Add again "status" data channel to test that it is correctly disposed also in that case (which + // should not happen anyway even if it was added by the remote peer, but just in case) + peerConnectionObserverArgumentCaptor.value.onDataChannel(mockedStatusDataChannel) + + for (j in 0.. = + ArgumentCaptor.forClass(PeerConnection.Observer::class.java) + + Mockito.`when`( + mockedPeerConnectionFactory!!.createPeerConnection( + any(PeerConnection.RTCConfiguration::class.java), + peerConnectionObserverArgumentCaptor.capture() + ) + ).thenReturn(mockedPeerConnection) + + val mockedStatusDataChannel = Mockito.mock(DataChannel::class.java) + + Mockito.`when`(mockedStatusDataChannel.label()).thenAnswer(ReturnValueOrThrowIfDisposed("status")) + Mockito.`when`(mockedStatusDataChannel.state()) + .thenAnswer(ReturnValueOrThrowIfDisposed(DataChannel.State.OPEN)) + Mockito.`when`(mockedStatusDataChannel.send(any())).thenAnswer(ReturnValueOrThrowIfDisposed(true)) + Mockito.`when`(mockedPeerConnection!!.createDataChannel(eq("status"), any())) + .thenReturn(mockedStatusDataChannel) + + peerConnectionWrapper = PeerConnectionWrapper( + mockedPeerConnectionFactory, + ArrayList(), + MediaConstraints(), + "the-session-id", + "the-local-session-id", + null, + true, + true, + "video", + mockedSignalingMessageReceiver, + mockedSignalingMessageSender + ) + + val dataChannelMessageCount = 5 + + // Exceptions thrown in threads are not propagated to the main thread, so it needs to be explicitly done + // (for example, for IllegalStateExceptions when using a disposed data channel). + var exceptionSend: Exception? = null + + val sendThread = thread { + try { + for (j in 0.. = + ArgumentCaptor.forClass(PeerConnection.Observer::class.java) + + Mockito.`when`( + mockedPeerConnectionFactory!!.createPeerConnection( + any(PeerConnection.RTCConfiguration::class.java), + peerConnectionObserverArgumentCaptor.capture() + ) + ).thenReturn(mockedPeerConnection) + + val mockedStatusDataChannel = Mockito.mock(DataChannel::class.java) + Mockito.`when`(mockedStatusDataChannel.label()).thenAnswer(ReturnValueOrThrowIfDisposed("status")) + Mockito.`when`(mockedStatusDataChannel.state()).thenAnswer( + ReturnValueOrThrowIfDisposed(DataChannel.State.OPEN) + ) + Mockito.`when`(mockedPeerConnection!!.createDataChannel(eq("status"), any())) + .thenReturn(mockedStatusDataChannel) + + val statusDataChannelObserverArgumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(DataChannel.Observer::class.java) + + doNothing().`when`(mockedStatusDataChannel) + .registerObserver(statusDataChannelObserverArgumentCaptor.capture()) + + peerConnectionWrapper = PeerConnectionWrapper( + mockedPeerConnectionFactory, + ArrayList(), + MediaConstraints(), + "the-session-id", + "the-local-session-id", + null, + true, + true, + "video", + mockedSignalingMessageReceiver, + mockedSignalingMessageSender + ) + + val mockedDataChannelMessageListener = Mockito.mock(DataChannelMessageListener::class.java) + peerConnectionWrapper!!.addListener(mockedDataChannelMessageListener) + + // Exceptions thrown in threads are not propagated to the main thread, so it needs to be explicitly done + // (for example, for IllegalStateExceptions when using a disposed data channel). + var exceptionOnMessage: Exception? = null + + val onMessageThread = thread { + try { + // It is assumed that, even if its data channel was disposed, its buffers can be used while there + // is a reference to them, so no special mock behaviour is added to throw an exception in that case. + statusDataChannelObserverArgumentCaptor.value.onMessage( + dataChannelMessageToBuffer(DataChannelMessage("audioOn")) + ) + + statusDataChannelObserverArgumentCaptor.value.onMessage( + dataChannelMessageToBuffer(DataChannelMessage("audioOff")) + ) + } catch (e: Exception) { + exceptionOnMessage = e + } + } + + val removePeerConnectionThread = thread { + peerConnectionWrapper!!.removePeerConnection() + } + + onMessageThread.join() + removePeerConnectionThread.join() + + if (exceptionOnMessage !== null) { + throw exceptionOnMessage!! + } + + Mockito.verify(mockedStatusDataChannel).registerObserver(any()) + Mockito.verify(mockedStatusDataChannel).dispose() + Mockito.verify(mockedStatusDataChannel, atLeast(0)).label() + Mockito.verify(mockedStatusDataChannel, atLeast(0)).state() + Mockito.verifyNoMoreInteractions(mockedStatusDataChannel) + Mockito.verify(mockedDataChannelMessageListener, atMostOnce()).onAudioOn() + Mockito.verify(mockedDataChannelMessageListener, atMostOnce()).onAudioOff() + Mockito.verifyNoMoreInteractions(mockedDataChannelMessageListener) + } + } +} diff --git a/app/src/test/resources/RoomOverallExample_APIv1.json b/app/src/test/resources/RoomOverallExample_APIv1.json new file mode 100644 index 0000000..993f95c --- /dev/null +++ b/app/src/test/resources/RoomOverallExample_APIv1.json @@ -0,0 +1,66 @@ +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": { + "id": 6410, + "token": "juwd77g6", + "type": 1, + "name": "marcel", + "displayName": "Marcel", + "objectType": "", + "objectId": "", + "participantType": 1, + "participantInCall": false, + "participantFlags": 0, + "readOnly": 0, + "count": 0, + "hasPassword": false, + "hasCall": false, + "canStartCall": true, + "lastActivity": 1727098966, + "lastReadMessage": 92320, + "unreadMessages": 0, + "unreadMention": false, + "isFavorite": false, + "notificationLevel": 1, + "lobbyState": 0, + "lobbyTimer": 0, + "lastPing": 1727185155, + "sessionId": "0", + "participants": { + "marcel": { + "name": "marcel", + "type": 1, + "call": 0, + "sessionId": "tonIuryMpwk5mR2h6lFqPC6M8m6hxdT4YN9X9I1v4i2LOJb9r3FzANx\/7HT0j6r8fzBFPJqnOrT\/mdvHzFlfqQsr6Gxo0\/aLkDfIRL32XlGIzficfLaBNCsyctPxwgHv0fwmJUOS2i0ONdC+QWyTJ4bpYw1Ch9hiybxCgEtss3xy9fbjPwWj3gLvpMpcH0OuNkfEwHqByhpiAb3xuu60\/6j671uwYiGe2Ba7PzLFJ3LVuVRXXTbeaZlVwTAioOu" + }, + "Testuser.Lastname": { + "name": "Testuser", + "type": 1, + "call": 0, + "sessionId": "0" + } + }, + "numGuests": 0, + "guestList": "", + "lastMessage": { + "id": 175430, + "token": "juwd77g6", + "actorType": "users", + "actorId": "marcel", + "actorDisplayName": "Marcel", + "timestamp": 1727182442, + "message": "test", + "messageParameters": [], + "systemMessage": "", + "messageType": "comment", + "isReplyable": true, + "referenceId": "" + } + } + } +} diff --git a/app/src/test/resources/RoomOverallExample_APIv2.json b/app/src/test/resources/RoomOverallExample_APIv2.json new file mode 100644 index 0000000..9148c6b --- /dev/null +++ b/app/src/test/resources/RoomOverallExample_APIv2.json @@ -0,0 +1,51 @@ +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": { + "id": 6410, + "token": "juwd77g6", + "type": 1, + "name": "marcel", + "displayName": "Marcel", + "objectType": "", + "objectId": "", + "participantType": 1, + "participantFlags": 0, + "readOnly": 0, + "hasPassword": false, + "hasCall": false, + "canStartCall": true, + "lastActivity": 1727098966, + "lastReadMessage": 92320, + "unreadMessages": 0, + "unreadMention": false, + "isFavorite": false, + "canLeaveConversation": true, + "canDeleteConversation": false, + "notificationLevel": 1, + "lobbyState": 0, + "lobbyTimer": 0, + "lastPing": 1727185155, + "sessionId": "0", + "guestList": "", + "lastMessage": { + "id": 175430, + "token": "juwd77g6", + "actorType": "users", + "actorId": "marcel", + "actorDisplayName": "Marcel", + "timestamp": 1727182442, + "message": "test", + "messageParameters": [], + "systemMessage": "", + "messageType": "comment", + "isReplyable": true, + "referenceId": "" + } + } + } +} diff --git a/app/src/test/resources/RoomOverallExample_APIv4.json b/app/src/test/resources/RoomOverallExample_APIv4.json new file mode 100644 index 0000000..59ef478 --- /dev/null +++ b/app/src/test/resources/RoomOverallExample_APIv4.json @@ -0,0 +1,83 @@ +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK" + }, + "data": { + "id": 181, + "token": "juwd77g6", + "type": 1, + "name": "marcel", + "displayName": "Marcel", + "objectType": "", + "objectId": "", + "participantType": 1, + "participantFlags": 0, + "readOnly": 0, + "hasPassword": false, + "hasCall": false, + "callStartTime": 0, + "callRecording": 0, + "canStartCall": true, + "lastActivity": 1727098966, + "lastReadMessage": 92320, + "unreadMessages": 0, + "unreadMention": false, + "unreadMentionDirect": false, + "isFavorite": false, + "canLeaveConversation": true, + "canDeleteConversation": false, + "notificationLevel": 1, + "notificationCalls": 1, + "lobbyState": 0, + "lobbyTimer": 0, + "lastPing": 1727185155, + "sessionId": "0", + "lastMessage": { + "id": 92320, + "token": "3853979093", + "actorType": "users", + "actorId": "marcel", + "actorDisplayName": "Marcel", + "timestamp": 1726673204, + "message": "Test", + "messageParameters": [], + "systemMessage": "", + "messageType": "comment", + "isReplyable": true, + "referenceId": "", + "reactions": {}, + "expirationTimestamp": 0, + "markdown": true + }, + "sipEnabled": 0, + "actorType": "users", + "actorId": "marcel2", + "attendeeId": 5810, + "permissions": 254, + "attendeePermissions": 0, + "callPermissions": 0, + "defaultPermissions": 0, + "canEnableSIP": false, + "attendeePin": "", + "description": "test", + "lastCommonReadMessage": 92320, + "listable": 0, + "callFlag": 0, + "messageExpiration": 0, + "avatarVersion": "143a9df3", + "isCustomAvatar": false, + "breakoutRoomMode": 0, + "breakoutRoomStatus": 0, + "recordingConsent": 0, + "mentionPermissions": 0, + "isArchived": false, + "status": "away", + "statusIcon": "👻", + "statusMessage": "buuuuh", + "statusClearAt": null + } + } +} diff --git a/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..ca6ee9c --- /dev/null +++ b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5c7a6f0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,51 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2024 Marcel Hibbe + * SPDX-FileCopyrightText: 2021 Andy Scherzinger + * SPDX-FileCopyrightText: 2017-2019 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + + ext { + kotlinVersion = '2.2.20' + } + + repositories { + google() + gradlePluginPortal() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" + classpath "org.jetbrains.kotlin:kotlin-serialization:${kotlinVersion}" + classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.4.2' + classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8" + classpath "org.jlleitschuh.gradle:ktlint-gradle:13.1.0" + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +configurations.configureEach { + exclude group: 'org.jetbrains', module: 'annotations-java5' // via prism4j, already using annotations explicitly + // check for updates every build + resolutionStrategy.cacheChangingModulesFor 3600, 'seconds' +} + +allprojects { + repositories { + google() + mavenCentral() + maven { url = 'https://jitpack.io' } + } +} + +tasks.register('clean', Delete) { + delete rootProject.layout.buildDirectory +} diff --git a/contribute/developer-certificate-of-origin b/contribute/developer-certificate-of-origin new file mode 100644 index 0000000..a6bbb98 --- /dev/null +++ b/contribute/developer-certificate-of-origin @@ -0,0 +1,35 @@ +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/contribute/developer-certificate-of-origin.license b/contribute/developer-certificate-of-origin.license new file mode 100644 index 0000000..6952765 --- /dev/null +++ b/contribute/developer-certificate-of-origin.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2004, 2006 The Linux Foundation and its contributors +SPDX-License-Identifier: CC0-1.0 diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 0000000..a516d58 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,493 @@ +# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later +build: + maxIssues: 80 + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +processors: + active: true + exclude: [] + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ClassCountProcessor' + # - 'PackageCountProcessor' + # - 'KtFileCountProcessor' + +console-reports: + active: true + exclude: [] + # - 'ProjectStatisticsReport' + # - 'ComplexityReport' + # - 'NotificationReport' + # - 'FindingsReport' + # - 'BuildFailureReport' + +comments: + active: true + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$) + UndocumentedPublicClass: + active: false + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + UndocumentedPublicFunction: + active: false + +complexity: + active: true + ComplexCondition: + active: true + threshold: 5 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + ComplexMethod: + active: true + threshold: 10 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + excludes: ['**/androidTest/**'] + LabeledExpression: + active: false + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + excludes: ['**/androidTest/**'] + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + MethodOverloading: + active: false + threshold: 6 + NestedBlockDepth: + active: true + threshold: 4 + StringLiteralDuplication: + active: false + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + thresholdInFiles: 15 + thresholdInClasses: 15 + thresholdInInterfaces: 15 + thresholdInObjects: 15 + thresholdInEnums: 11 + ignoreDeprecated: true + ignorePrivate: false + ignoreOverridden: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: false + methodNames: + - toString + - hashCode + - equals + - finalize + InstanceOfCheckForException: + active: false + NotImplementedDeclaration: + active: false + PrintStackTrace: + active: false + RethrowCaughtException: + active: false + ReturnFromFinally: + active: false + SwallowedException: + active: false + ignoredExceptionTypes: + - InterruptedException + - NumberFormatException + - ParseException + - MalformedURLException + ThrowingExceptionFromFinally: + active: false + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: false + exceptions: + - IllegalArgumentException + - IllegalStateException + - IOException + ThrowingNewInstanceOfSameException: + active: false + TooGenericExceptionCaught: + active: true + exceptionNames: + - ArrayIndexOutOfBoundsException + - Error + - Exception + - IllegalMonitorStateException + - NullPointerException + - IndexOutOfBoundsException + - RuntimeException + - Throwable + allowedExceptionNameRegex: "^(_|(ignore|expected).*)" + TooGenericExceptionThrown: + active: true + exceptionNames: + - Error + - Exception + - Throwable + - RuntimeException + +formatting: + active: true + android: false + ChainWrapping: + active: true + CommentSpacing: + active: true + Filename: + active: true + FinalNewline: + active: true + ImportOrdering: + active: false + Indentation: + active: true + indentSize: 4 + continuationIndentSize: 4 + MaximumLineLength: + active: true + maxLineLength: 120 + ModifierOrdering: + active: true + NoBlankLineBeforeRbrace: + active: true + NoConsecutiveBlankLines: + active: true + NoEmptyClassBody: + active: true + NoLineBreakAfterElse: + active: true + NoLineBreakBeforeAssignment: + active: true + NoMultipleSpaces: + active: true + NoSemicolons: + active: true + NoTrailingSpaces: + active: true + NoUnitReturn: + active: true + NoUnusedImports: + active: true + NoWildcardImports: + active: true + PackageName: + active: true + ParameterListWrapping: + active: true + indentSize: 4 + SpacingAroundColon: + active: true + SpacingAroundComma: + active: true + SpacingAroundCurly: + active: true + SpacingAroundKeyword: + active: true + SpacingAroundOperators: + active: true + SpacingAroundParens: + active: true + SpacingAroundRangeOperator: + active: true + StringTemplate: + active: true + +naming: + active: true + ClassNaming: + active: true + classPattern: '[A-Z$][a-zA-Z0-9$]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + functionPattern: '^([a-z$A-Z][a-zA-Z$0-9]*)|(`.*`)$' + excludeClassPattern: '$^' + ignoreOverridden: true + excludes: + - "**/*Test.kt" + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + MatchingDeclarationName: + active: true + MemberNameEqualsClassName: + active: false + ignoreOverridden: true + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + ignoreOverridden: true + +performance: + active: true + ArrayPrimitive: + active: false + ForEachOnRange: + active: true + SpreadOperator: + active: true + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + DuplicateCaseInWhenExpression: + active: true + EqualsAlwaysReturnsTrueOrFalse: + active: false + EqualsWithHashCodeExist: + active: true + ExplicitGarbageCollectionCall: + active: true + InvalidRange: + active: false + IteratorHasNextCallsNextMethod: + active: false + IteratorNotThrowingNoSuchElementException: + active: false + LateinitUsage: + active: false + ignoreAnnotated: [] + ignoreOnClassesPattern: "" + UnconditionalJumpStatementInLoop: + active: false + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: false + UnsafeCast: + active: false + UselessPostfixExpression: + active: false + WrongEqualsTypeParameter: + active: false + +style: + active: true + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: 'to' + EqualsNullCall: + active: false + EqualsOnSignatureLine: + active: false + ExplicitItLambdaParameter: + active: false + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenComment: + active: true + values: + - "TODO:" + - "FIXME:" + - "STOPSHIP:" + ForbiddenImport: + active: false + imports: [] + ForbiddenVoid: + active: false + FunctionOnlyReturningConstant: + active: false + ignoreOverridableFunction: true + excludedFunctions: 'describeContents' + LoopWithTooManyJumpStatements: + active: false + maxJumpCount: 1 + MagicNumber: + active: true + ignoreNumbers: [ "-1","0","1","2" ] + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + excludes: + - "**/*Test.kt" + MandatoryBracesIfStatements: + active: false + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + MayBeConst: + active: false + ModifierOrder: + active: true + NestedClassesVisibility: + active: false + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + OptionalWhenBraces: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: false + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 2 + excludedFunctions: "equals" + excludeLabeled: false + excludeReturnFromLambda: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: false + SpacingBetweenPackageAndImports: + active: false + ThrowsCount: + active: true + max: 2 + TrailingWhitespace: + active: false + UnderscoresInNumericLiterals: + active: false + acceptableLength: 5 + UnnecessaryAbstractClass: + active: false + ignoreAnnotated: ["dagger.Module"] + UnnecessaryApply: + active: false + UnnecessaryInheritance: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedPrivateClass: + active: false + UnusedPrivateMember: + active: false + allowedNames: "(_|ignored|expected|serialVersionUID)" + UseDataClass: + active: false + ignoreAnnotated: [] + UtilityClassWithPublicConstructor: + active: false + VarCouldBeVal: + active: false + WildcardImport: + active: true + excludeImports: + - 'java.util.*' + - 'kotlinx.android.synthetic.*' diff --git a/docs/TURN.md b/docs/TURN.md new file mode 100644 index 0000000..ba66772 --- /dev/null +++ b/docs/TURN.md @@ -0,0 +1,96 @@ + +### Background +The configuration of Nextcloud Talk mainly depends on your desired usage: +- As long as it shall be used only **within one local network**, nothing should be needed at all. Just verify that all browsers support the underlying [WebRTC](https://en.wikipedia.org/wiki/WebRTC) protocol (all famous ones do on current versions) and you should be good to go. +- Talk tries to establish a direct [peer-to-peer (P2P)](https://en.wikipedia.org/wiki/Peer-to-peer) connection, thus on connections **throughout the local network** (behind a [NAT](https://en.wikipedia.org/wiki/Network_address_translation)/router), clients do not only need to know each others public IP, but their local IP as well. Processing this, is the job of a [STUN](https://en.wikipedia.org/wiki/STUN) server. As there is one preconfigured for Nextcloud Talk, still nothing need to be done. +- In some cases, e.g. **in combination with firewalls or [symmetric NAT](https://en.wikipedia.org/wiki/Network_address_translation#Symmetric_NAT)** a STUN server will not work as well, and then a so called [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT) server is needed. Now no direct P2P connection is established, but all traffic is relayed through the TURN server, thus additional (at least internal) traffic and resources are needed. +- Nextcloud Talk will try direct P2P in the first place, use STUN if needed and TURN as last resort fallback. Thus to be most flexible and guarantee functionality of your Nextcloud Talk instance in all possible connection cases, you most properly want to setup a TURN server. + +### Install and setup _coturn_ as TURN server +1. **Download/install** + - On **Debian and Ubuntu** there are official repository packages available: +`sudo apt install coturn` + - For **Fedora**, an official package it is planned, as far as I can see. For this **and other** cases check out: https://github.com/coturn/coturn/wiki/Downloads + +2. **Make coturn run as daemon on startup** + - On **Debian and Ubuntu** you just need to enable the deployed init.d service by adjusting the related environment variable: + - `sudo sed -i '/TURNSERVER_ENABLED/c\TURNSERVER_ENABLED=1' /etc/default/coturn` + - On **Debian Buster** the most current package update implements a systemd unit, which does not use `/etc/default/coturn` but is enabled automatically after install. To check whether a systemd unit is available: + - `ls -l /lib/systemd/system/coturn.service` + - On **other OS/distributions**, if you installed coturn manually, you may want to setup an init.d/systemd unit or use another method to run the following during boot: + - `/path/to/turnserver -c /path/to/turnserver.conf -o` + - `-o` starts the server in daemon mode, `-c` defines the path to the config file. + +3. **Configure _turnserver.conf_ for usage with Nextcloud Talk** +At last you need to adjust the TURN servers configuration file to work with Nextcloud Talk. On Debian and Ubuntu, it can be found at `/etc/turnserver.conf`. The configuration depends on if you want to use TLS for secure connection or not. You may want to start without TLS for testing and then switch, if everything is working fine: + - **Without TLS** uncomment/adjust the following settings. Choose the listening port, e.g. `3478` (default for non-TLS) or `5349` (default for TLS) and an authentication secret, where a random hex is recommended: `openssl rand -hex 32`: + + listening-port= + fingerprint + use-auth-secret + static-auth-secret= + realm=your.domain.org + total-quota=100 + bps-capacity=0 + stale-nonce + no-loopback-peers + no-multicast-peers + - **With TLS** you need to provide the path to your certificate and key files as well and it is highly recommended to adjust the cipher list: + + tls-listening-port= + fingerprint + use-auth-secret + static-auth-secret= + realm=your.domain.org + total-quota=100 + bps-capacity=0 + stale-nonce + cert=/path/to/your/cert.pem + pkey=/path/to/your/privkey.pem + cipher-list="ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AES:RSA+3DES:!ADH:!AECDH:!MD5" + no-loopback-peers + no-multicast-peers + + Note that in case of TLS you only need to set `tls-listening-port`, otherwise only `listening-port`. Nextcloud Talk uses a single port only, thus the _alternative_ ports offered by the settings file can be ignored. + + I added a working cipher example here that is also used within most other guides. But it makes totally sense to **use the cipher-list from your Nextcloud webserver** to have the same compatibility versus security versus performance for both. + + If you want it damn secure, you can also configure a custom [Diffie-Hellman](https://en.wikipedia.org/wiki/Diffie–Hellman_key_exchange) file and/or disable TLSv1.0 + TLSv1.1. But again, it does not make much sense for my impression to handle it different here than for Nextcloud itself. Just decide how much compatibility you need and security/performance you want and configure webserver + coturn the same: + + dh-file=/path/to/your/dhparams.pem + no-tlsv1 + no-tlsv1_1 + - If your TURN server is running **not behind a NAT**, but with direct www connection and **static public IP**, than you can limit the IPs it listens and answers by setting those as `listening-ip` and `relay-ip`. On larger deployments it is recommended to run your TURN server on a dedicated machine that is directly accessible from the internet. + - The following settings can be used to adjust the **logging behaviour**. On SBCs with SDcards you may want to adjust this, as by default coturn logs veeery much :wink:. The config file explains everything very well: + + no-stdout-log + log-file=... + syslog + simple-log + +4. `sudo systemctl restart coturn` or corresponding restart method + +5. **Configure Nextcloud Talk to use your TURN server** +Go to Nextcloud admin panel > Talk settings. Btw. if you already have your own TURN server, you can and may want to use it as STUN server as well: + + STUN servers: your.domain.org: + TURN server: your.domain.org: + TURN secret: + UDP and TCP + Do not add `http(s)://` here, this causes errors, the protocol is simply a different one. Also `turn:` or something as prefix is not needed. Just enter the bare `domain:port`. + +6. **Port opening/forwarding**\ +The TURN server on `` needs to be available for all Talk participants, so you need to open it to the web and if your TURN server is running **behind a NAT**, forward it to the related machine. + +### What else + Nextcloud Talk is still based on the Spreed video calls app (just got renamed on last major update) and thus the Spreed.ME WebRTC solution. For this reason all guides about how to configure coturn for one of them, applies to all of them. + +**Futher reference** +- https://github.com/spreedbox/spreedbox/wiki/Use-TURN-server +- https://github.com/nextcloud/spreed/issues/667 + +**Thanks to** @fancycode and @mario for some clarifications about all of this and if you don't mind, please review the HowTo for possible mistakes or wrong understandings. +Thanks as well to @sushidave for motivating me to write this HowTo :slightly_smiling_face:. diff --git a/docs/branching.png b/docs/branching.png new file mode 100644 index 0000000000000000000000000000000000000000..1b5605509dcfefcc1f6b9febe085ed81eca82b3d GIT binary patch literal 22626 zcma&ObzD`?7cRUhL6A^71O(}Bq`SKtY3Wuv1*D|AOG>&M=|-dk1f)Z{n|JvB?!AA% z=cBmKVV{}VGi%nY^*qlqR7pV+6`24Tf*@3BDKQlYf}H}_{YbFjXML<{3k1OsTB>Qe zXvxd*nAqDg8kyP~n=!iCI)L9p5TBr%gOQ1~nG310nT4etKiOeR2N|iQDLd$^h+(Db0&Xk;(u+3nK_#{Svt5_+S`#nwP|E* z@9M%&M#l8i3Df`S1q_$z|BV;C`F~GrW(OYA2|N#hOzAF|rT|M(Q6*;;NfA=QA(;+LOjMXn_Il0oNMs+Z8LepCGYSJqs$Igx%E7R9Nc6NQx-q zcBX1^pMrQ3#Hh)<82O3@5rPio`Sn)0;?^i0ShP{D+TL5@e(Wjb*6WoKk$pZqydzIh zBXcdVDQh!vwi?hwd~D}(cXjmleXw+E1Z!90NZyMTV=axTfloN6TiQu_;^qlN1lB#$ z(MC#B-_@dIB0`B1V%}O@JlY$cCoia%*qsV>hZ}jF39q4Hodh3y+u#LZRtj{hH^+AP zz?)>{Zqex+zWLqoceOb;=<=@Y$ofUe9yz*@9Q1xKrZ6@8@>t!}aqyPvhfE1}9JNxfI47r(^+2i%Nua~#? z!i`InRz&}r^jj6lDb#>w$jd?#0yjG6=H@D-a|@Hjd|=nn&C=D=+uPdGG&Ed(i5-|- z?*DrZ+m95f3#V8uH8$4iWPPy7d8@sDQdU;h+1YuaO#@>p0fWCc?&(`{kDe*HS^ zczoEIEPVT_xTK`Sez_efAf5M<-FPOyX1U%=KUc5WgCZEjA^kB<-czW3*|r(5F; z^z=kTL_@>FzIVs{>+7b)YF6gvGe3VG{j2EsipL_NEx^HX-mvVWN^G~(nlfq*n!P_> z>u>YC{5H7*nqH{0o(;vM3JVXvJ=+m|+m$9-G#s$VhygD=>cE|TqMIc|ih%?Vb;eT2 z;xOpszaRpA8n$~iRj(@w-eX~7Q@jd=g@w)JbALrZkeZ(U-HQzJlX|;)d^IvMvcJ$! zYQV?Fc6xi*p~>%kbGDkz5Vu}1qSZfmtNy;10HyM z3|L_d_#7m7Sg5m1w*dp*{p#oq^!Mic-w+AE#~BiIwAzajB=Qm;{~A0a?f9u1A3aaCTB2N=R+3bnZ0YrL;Ws1{Cs?RIZC8RDu#ykR#weM-Oo0n z2nY$a>1UWa?u@fhf=L4dM?JuU6ZH1>Di=?koSbxrpu<66EE6BVz;|XV4CUk^wCEMU zOGd`y{)U4%O7k1eJt~QS-duKUe0e0tG1Tb?@oU>{1+0X71^!hs=+Us^u9jCl$Dk3 zg7#Y7j>+D<5q9sJo12@SuDh>545)+&MhE5^si&tWMmAAp(oauM4*>&yOG)YB<`$on z#H3wqx=?Rx=yOfsFQi0`#rttDTBg8CQ4@{iWvJK?O$!AD#dcQ7vvOMJzTN54?(Xgc z3szQEIeB^Bi+R$b(@vPer^yhPkf6m11FeYT(<@}~VB_FuXlPJkA-{w_K0Q@MW8!!C z>j!KEIS7Jq!Vw^)@>3&wdvvJr(;+dGU0l4gJ{VW_Kybfrk8G;NnpU85tRIaBzP7_`&<>U~i$}@OPC-UT*Hq#s0t5RaUp7zZ#F& zvIQ>&r{>ZI|W}Qv-vg$wI}p*4CTLLqjz+HFfpr3PV8%3h54win;{0a(o;! zyb(prPfJIKf{I#KRV6JaN5pE11pNiu&A`BblasSV)7r+y>*3B_Uw;u8z?@CJ5feec z3*`S65(!*jA5$upS}G5y>M`MMGD{v5&N32_FRPyLE&3G25&Ha!p}JlD-u?O zSPcfb5)3UisW%rdINQR#MrbjjwaMkaV=#B_V5iq&mk<-wTlw=9T$Y{M-3lhvr}p!e`XEZZLOhs_xtOy zyI`6TBV~qG+a+ZTId7J;-KLYFCX!q1X5{f+a{hbXzEz=%eB#XvE&c(iAL4zaNUZ5} zbLYe5xEe=OT<=xitU3}to3|xc73x}hpD+?iJ%lx!Sv@V7zuj5?a{__5g_$L_m z7w!uZ40!x53oc||#UhccH!F_qGqt3;5_Lj1CFvD2M5|`P@;bYc{a(C3W+XS#h~rB# zdSL$ADa6nSv1oq3cy2vPTHw8FA9U?c>f2Us;8m8r;m%7r_*rD27_->y)wAbWts~3b z>BiYN!V+jt7QpjoF`$oyicLL>1D?oE-Z#xYpK?9E;Rd3&}M-S}op-{_|tH8O@QojGb zQ`SoTj1-dN&5F||SINL%v-9|rIZkSU6Mphh;#G>@C$2(zWYaar#>QOAjX~q`o_G-? zXZEiO6h+QI5})R6=2CI!(X*n-#Q2aq3HuFaDsuRS)L#;NR;wi+#mmb41A@Sbz9`?5 zrWOOWBD9c})IYZalBz~2usfHUskE9^J3S_Gd_1jThZO$Zk0d4JgLCnBo2tGz+G}Fl z?n$Q#1v9iHlhRkgOo5TsmCRE#@2~twi%jHs;+!d346eDS{I6|{nD#eFlGsrNQYVhj ztrT5e3GA!-?nkm0s|RF@TC(!@*&7e-H}Us1S`$jolg3`&@r!eqQW;g*M>{Gc2ETaz zFR%%0iI3YxFeYco7%V?S3pNA@)4GUKcv?d4%68iOn0*mP!xW}6`5?$#O@w=QHtfMw zG@A0sPfYo9^I5v9o@xza@E?RaLh$p(E_|OUZ}e~>(w?P(RQNN!BUEpFaO;iV3S2-o zTD&Ti2uZ5anbj|5j{1Tb{WC)Bf5u@GKWb99wT&#|HjUra5oqcJl49ctfb|y=NK`vB z3h-&@eNJR6Yh@gL!6CGu^Md#6JF`bXZ4|b;tTd8Jz<&*!j*`AhWj_?UsEsc?r4iR- zX)f%e?n(NGT@q0)OV*^4J&vS(=8C^F!x!!U;|NArsb3^2q%mImvZzto^O~a}oVQV8 zpUJRT{}G%rH~)15=y|O=IR{HjkHH?%E}aFr-iyL|T-$?>2Nz zTQBCiZKoiuwP*3oNUc96e>X@oP4oPsKHrC}C#Spg%77S`D+f2?6Z@Nv>#1e6?ra=1 z#obS&bqds~#5!zJEq&aCxrw$(8Vr9P8ueVIC{{6XZms4mBuj*D9#RtT><&k6m|LS$M3#H2qgitmX?~77K)3!J2dXeVHOpiqM`}dPvcI}0w zup+$r$o+b8?XQJAFTu`I_410c<0O^l$*Gd(l0oK5&_>2KRr;6&m-%m2X~e#k`tJ@3 z8QW{gcN#6fW`gD)_z3cLkh@_2TMMma0s33Ya=HDtXnt=Pzx!N_2Lq<$WCn~&~U$wv44E2ApubD|&jU43!I9NYew zBnVosv(}fMPfrtuP@UiN*BMNu<2pie7c3k(6QMsmOwck4I zPI5)Y>?@So8My(BidvUA(blJ8{pD^aAJ=Gjdzs)@+8&7cQq;od0GM*QD(L|2hZxCjQHuGq#^16ze!3d*rrod48erl8vE@h=6p3BY1qg%n-0V2S?{2p+N+rtD$%b4g?D*o)khNEY8=T^!*5ioo<)^!vCHu%kci>T*Nj&! zgJJ&P&b$?ZexGHZ@s*v{EYnGWAv2KPJuUK1WU;>E#zKD((oq8Pcg zo5HLht;5$_R&tYHb3RVH1{;ZDn7T2$3zZQ7qh*_v9e(lc{#c#P9;eH5*$(B&OwKYf zpa;*2^$;o|c?2-ZonqoikFNCz`NhF41)jXJdlUD{^Y-J)`#j$NT-l{RRLN4m-d_k> zo!=fI$$F-H7;>F=>;Lo|oTAWg4Bcu`=M&OZ$HGE4xu8Kf*uPm`2l;I8L`JVow4=`t zUo!i;F#WejIFTU3MTYC0l=^i?^O7wr5@*pF_*g}n0 zSvZ(@@^%l_1qrYiC}W$V{J_WhW&&XuIox0tP?D+QY1zUKG5h>)W!#cpvw-2~A(KR-VmEiEE>@!i$& z<-sD#^XKjtdvg|9-u0gj4Uz>(i`GaXuVcmIu`TRWWa_wnQ_%fP4-UJ+)cix$+b`bC zU8p`^J|PBYTD4e=6QiJ@poKVZjb+5f#_sLyh2t`nBv{(m=rz0SG`s#&QBiSncJ}u6 z2K}BUGP(Zk^iN_kG`Fx21QGwiV$1F2VaKP#OkqaYfM#-dzw_qU{w7PXJ_M02Y)zaC zY31&PZK9#mvK82!InbiNaCeAE4#)-dT0Sx)$RI#ky1G$#EMz=9I%U7_Z!bX_PNkR$ zvii}H5iljjCML2nGE-r%1%3FqxqnYjV?)eN>r#@E{dtm61gos1YqP(9U(Fi&Mv@3F zEiOLgvfA3(I>?0u1@CxxW~Qb-&^qk>t^zY_(B@I5Qzt`;;UeVCO9I>3%-h@XL4o)! z-@zUGcoFfFK}9%lG;zx6K3tu;p1(>KY=#L8M&~2;-JL47ocud_3w#kE4{o*+??9vbLs@ zPt8}VUBXI_i2-S{wuZ*H;=q%xAQU={??|Yq`VDrAK`6LtzDxR{tnParIUMeCm+A0v zi1{Ha%_sea6{Wa_LDzZV|7x)n%vk4s708B)t>PMb&e-T9ORJu>H)Vly!-j+4F(c0A7Vit zFM!E_^s<)z@87@CF)?ovWD-K`msg-3Zap_Qx8Yc@HmNKo!HG#K7tcz%LJA5B+Sy$-_=z)}O^i-3PU;k^SmwR>-fRpd;Q-gqq6AwlsDb*+gDX-_vS@Ar#YLi}ObhMJT_H!ti z-7>bIU}M?$ks)cAkdUxKuZh*}4>y3mAR$XjOJn2Q6B^(EG!*I(pq}2|goFf;-uHJV z!Qt0s@O~=SYjOhM6rEvRBB-$=7;9F@u7F#>U2`d{K${@Sx7MV-XP% z0A(OR*^@ghANS|0OeEjG&u(b=BqGxFBM>p6qVm(>Qe`Fc#v0fr9v&Vy*Vm}8d0c*1 z8fE!DxR+*dV@$mYAWh@6ov$?N2FQz!zPNI#31eNS_4?H9K=eS4yTLZnzM3~>|ed<{X#HXcdkum<>|j)LAanjJl&E3dS)G(cO{ z`lBSNqN1at0c;1bsnsN~e~$Y}62fO`07Um;Ao`8Ey1EUguDkhKU*xxM-v~KvAjoE+ zet#>=7wmOsfRoIW>CVV9ilF zF7XZqO5U<}w~0jrbGEv_-v+t_pk1~6IO~UDM0g`%0D^%g0SW_NYE((|+N|$8{rdX) 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/branching.svg.license b/docs/branching.svg.license new file mode 100644 index 0000000..7548bd1 --- /dev/null +++ b/docs/branching.svg.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/docs/gplayDebugBuildVariant.png b/docs/gplayDebugBuildVariant.png new file mode 100644 index 0000000000000000000000000000000000000000..421e99ffede4430ac8cb15ab702fe31f51c93682 GIT binary patch literal 12930 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 literal 0 HcmV?d00001 diff --git a/docs/gplayDebugBuildVariant.png.license b/docs/gplayDebugBuildVariant.png.license new file mode 100644 index 0000000..7548bd1 --- /dev/null +++ b/docs/gplayDebugBuildVariant.png.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/docs/grantNotificationPermissionAfterInstall.png b/docs/grantNotificationPermissionAfterInstall.png new file mode 100644 index 0000000000000000000000000000000000000000..fdb83b6fa96f0efa66a1f417a0acf24c3231ed7d GIT binary patch literal 12356 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 Zy_=VlyZ0tei#(i!sH^JSDpj@){~sPtgmnM_ literal 0 HcmV?d00001 diff --git a/docs/grantNotificationPermissionAfterInstall.png.license b/docs/grantNotificationPermissionAfterInstall.png.license new file mode 100644 index 0000000..7548bd1 --- /dev/null +++ b/docs/grantNotificationPermissionAfterInstall.png.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/docs/ignoreBatteryOptimizationDialog.png b/docs/ignoreBatteryOptimizationDialog.png new file mode 100644 index 0000000000000000000000000000000000000000..8e39630c8c36d03239f71096caae3c06f83c5ef2 GIT binary patch literal 46956 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 literal 0 HcmV?d00001 diff --git a/docs/ignoreBatteryOptimizationDialog.png.license b/docs/ignoreBatteryOptimizationDialog.png.license new file mode 100644 index 0000000..7548bd1 --- /dev/null +++ b/docs/ignoreBatteryOptimizationDialog.png.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/docs/ignoreBatteryOptimizationSelectAllApps.png b/docs/ignoreBatteryOptimizationSelectAllApps.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf763438959670bac7ebb1e07e03f69f34bc61d GIT binary patch literal 37000 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 literal 0 HcmV?d00001 diff --git a/docs/ignoreBatteryOptimizationTurnOffSwitch.png.license b/docs/ignoreBatteryOptimizationTurnOffSwitch.png.license new file mode 100644 index 0000000..7548bd1 --- /dev/null +++ b/docs/ignoreBatteryOptimizationTurnOffSwitch.png.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/docs/notificationSettingsExample.png b/docs/notificationSettingsExample.png new file mode 100644 index 0000000000000000000000000000000000000000..696de9fbaa234c414a77e69d9f040b77a67037a2 GIT binary patch literal 53839 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 D(~59V literal 0 HcmV?d00001 diff --git a/docs/notificationSettingsExample.png.license b/docs/notificationSettingsExample.png.license new file mode 100644 index 0000000..7548bd1 --- /dev/null +++ b/docs/notificationSettingsExample.png.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/docs/notifications.md b/docs/notifications.md new file mode 100644 index 0000000..6688560 --- /dev/null +++ b/docs/notifications.md @@ -0,0 +1,160 @@ + +# Debugging push notifications + +This list is intended to help users that have problems to receive talk notifications on their android phone. It may +not be complete. Please contribute to this list as you gain new knowledge. Just create an issue with the +"notification" label or create a pull request for this document. + +## 📱 Users +- Please make sure to install the app from the Google PlayStore. **The f-droid version doesn't support push + notifications.** + + [Download from Google Play](https://play.google.com/store/apps/details?id=com.nextcloud.talk2) + +- Only talk notifications will be delivered by the Talk app, for all other notifications install the Nextcloud Files + app from Google PlayStore. + + [Download from Google Play](https://play.google.com/store/apps/details?id=com.nextcloud.client) + +If your problem still occurs after checking all these hints, create an issue at https://github.com/nextcloud/talk-android/issues + +### 🤖 Check android settings + +First of all, please make sure that the following requirements are met: + +- Check that your phone has internet access + +- Check that your phone is not in "do not disturb" mode + +#### Grant permissions on install + +After the installation of Nextcloud Talk Android, dialogs appear asking you to grant the permissions. +Please note that the dialogs only appear once to respect the first decision of the users. +Depending on the Android version, only the dialogs appear that are necessary. + +##### Grant notification permission + +A dialog to enable notification permissions after install will only be shown for Android 13 and upwards. For older +Android versions notifications have to be enabled in the AppInfo settings. + +![Grant notification permission after install](/docs/grantNotificationPermissionAfterInstall.png "Grant notification permission after install") + +##### Ignore battery optimization + +For some smartphone models, ignoring battery optimization is not necessary while for others it is. If you absolutely want to be sure notifications are received, turning it off is highly recommended. + +![Ignore battery optimization dialog](/docs/ignoreBatteryOptimizationDialog.png "Ignore battery optimization dialog") + +Please follow the description to turn off battery optimization for the Talk app. In the available apps you can use the search so you don't have to scroll through the whole app list. + +![Ignore battery optimization - select all apps](/docs/ignoreBatteryOptimizationSelectAllApps.png "Ignore battery optimization - select all apps") + +Please note that the switch has to be *turned off*. + +![Ignore battery optimization - turn off switch](/docs/ignoreBatteryOptimizationTurnOffSwitch.png "Ignore battery optimization - turn off switch") + +#### Grant permissions in settings + +##### Regular warning + +If notifications settings are not set up correctly, there will be a warning in the conversation list. This is displayed regularly unless it is changed in the settings so that you are not reminded of incorrect settings. +If you select "Not now", the warning will be shown again after a few days if the settings are still wrong. When +selecting "Settings", the settings screen will appear. All incorrect settings will blink and their description is in +red color. + +##### Notification settings + +The notification settings can either be reached when selecting "Settings" in the regular notification warning or by +clicking on the user avatar in the right corner of the conversation list and by selecting "Settings". + +In the "Notifications" section, please change every setting that is marked with a red color. +Please take into account that the android settings might look different for each manufacturer. +You can also reach these settings by long pressing on the Nextcloud Android Talk App in Android itself and by selecting +"App info". + +![Warning that notifications are not set up correctly](/docs/notificationsNotSetUpCorrectlyWarning.png "Warning that notifications are not set up correctly") + +![Notification settings](/docs/notificationSettingsExample.png "Notification settings") + +##### Further help for permission settings + +If setting the above listed permission won't help, it might be that there are special settings for your phone. It +might be worth it to check what other messaging apps recommend to get their apps running on a certain smartphone and adapt this to the talk app. +Also [https://dontkillmyapp.com/](https://dontkillmyapp.com/) might be good starting point. + +### 🗨️ Check conversation settings +- In the conversation settings (in the upper right corner of a conversation), check that notifications are set to + "Always notify" or "Notify when mentioned" + + - Be aware that this is a per conversation setting. Set it for every conversation differently depending on your + needs. + +- Also be aware that notifications are not generated when you have an active session for a conversation. This also + applies for browser tabs that are open in the background, etc. + +### 🖥 Check server settings + +Run the `notification:test-push` command for the user who is logged in at the device that should receive the notification: + +```bash +sudo -u www-data php /var/www/yourinstance/occ notification:test-push --talk youruser +``` +Alternatively, you can check if push notifications are set up correctly on the server from the app’s Diagnosis screen: + +- Select the user avatar in the upper right corner of the conversation list +- Select "Settings" from the menu. +- In "Advanced" section of settings menu, select "Diagnosis" +- Click on “Test push notifications” button located at the bottom of the Diagnosis. + +If google play services are not available on the device, then you cannot see "Test push notifications" button in the Diagnosis screen. + +It should print something like the following: +``` +Trying to push to 2 devices + +Language is set to en +Private user key size: 1704 +Public user key size: 451 +Identified 1 Talk devices and 1 others. + +Device token:156850 +Device public key size: 451 +Data to encrypt is: {"nid":525210,"app":"admin_notification_talk","subject":"Testing push notifications","type":"admin_notifications","id":"614aeee4"} +Signed encrypted push subject +Push notification sent successfully +``` +This means the notifications are set up correctly on server side. A notification should be displayed on the device. +If there is no notification shown on the device, please focus on the settings of the talk android app. + +If it prints something like +``` +sudo -u www-data php /var/www/yourinstance/occ notification:test-push --talk youruser +No devices found for user +``` +try to remove the account from the Nextcloud Android Talk app and log in again. Afterwards try to run the command +again. + +If it prints +``` +There are no commands defined in the "notification" namespace. +``` +then the https://github.com/nextcloud/notifications app is not installed on your nextcloud instance. +The notification app is shipped and enabled by default, but could be missing in development environments or being disabled manually. +Install and enable the app by following the instructions at https://github.com/nextcloud/notifications#developers and +try again to execute the command. + +## 🦺 Developers/testers +- Be aware that the "qa"-versions that you can install by scanning the QR-code in a github pull request don't + support notifications! + +- When starting the talk app within Android Studio, make sure to select the "gplayDebug" build variant: + ![gplay debug build variant](/docs/gplayDebugBuildVariant.png "gplay debug build variant") + +- Especially after reinstalling the app, make sure to always check the android settings as they might be reset. diff --git a/docs/notificationsNotSetUpCorrectlyWarning.png b/docs/notificationsNotSetUpCorrectlyWarning.png new file mode 100644 index 0000000000000000000000000000000000000000..1168999d518de4358e6e1377b54b0cb57b6a7ce1 GIT binary patch literal 40482 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 + diff --git a/drawable_resources/icon-background.svg.license b/drawable_resources/icon-background.svg.license new file mode 100644 index 0000000..eb75407 --- /dev/null +++ b/drawable_resources/icon-background.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/drawable_resources/icon-foreground.svg b/drawable_resources/icon-foreground.svg new file mode 100644 index 0000000..9f00688 --- /dev/null +++ b/drawable_resources/icon-foreground.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/drawable_resources/icon-foreground.svg.license b/drawable_resources/icon-foreground.svg.license new file mode 100644 index 0000000..d497300 --- /dev/null +++ b/drawable_resources/icon-foreground.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/drawable_resources/icon-foreground_qa.svg b/drawable_resources/icon-foreground_qa.svg new file mode 100644 index 0000000..42f765c --- /dev/null +++ b/drawable_resources/icon-foreground_qa.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/drawable_resources/icon-foreground_qa.svg.license b/drawable_resources/icon-foreground_qa.svg.license new file mode 100644 index 0000000..d497300 --- /dev/null +++ b/drawable_resources/icon-foreground_qa.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/drawable_resources/other/account-circle.svg b/drawable_resources/other/account-circle.svg new file mode 100644 index 0000000..016d2e0 --- /dev/null +++ b/drawable_resources/other/account-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/drawable_resources/other/account-circle.svg.license b/drawable_resources/other/account-circle.svg.license new file mode 100644 index 0000000..7e8e3da --- /dev/null +++ b/drawable_resources/other/account-circle.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/circular_document.svg b/drawable_resources/other/circular_document.svg new file mode 100644 index 0000000..d140b84 --- /dev/null +++ b/drawable_resources/other/circular_document.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/drawable_resources/other/circular_document.svg.license b/drawable_resources/other/circular_document.svg.license new file mode 100644 index 0000000..f496122 --- /dev/null +++ b/drawable_resources/other/circular_document.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-FileCopyrightText: 2021 Andy Scherzinger +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/circular_group.svg b/drawable_resources/other/circular_group.svg new file mode 100644 index 0000000..0f282c5 --- /dev/null +++ b/drawable_resources/other/circular_group.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/drawable_resources/other/circular_group.svg.license b/drawable_resources/other/circular_group.svg.license new file mode 100644 index 0000000..963d648 --- /dev/null +++ b/drawable_resources/other/circular_group.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Andy Scherzinger +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/drawable_resources/other/circular_link.svg b/drawable_resources/other/circular_link.svg new file mode 100644 index 0000000..c6ea0bb --- /dev/null +++ b/drawable_resources/other/circular_link.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/drawable_resources/other/circular_link.svg.license b/drawable_resources/other/circular_link.svg.license new file mode 100644 index 0000000..664f021 --- /dev/null +++ b/drawable_resources/other/circular_link.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Andy Scherzinger +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/circular_location.svg b/drawable_resources/other/circular_location.svg new file mode 100644 index 0000000..262957d --- /dev/null +++ b/drawable_resources/other/circular_location.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/drawable_resources/other/circular_location.svg.license b/drawable_resources/other/circular_location.svg.license new file mode 100644 index 0000000..f496122 --- /dev/null +++ b/drawable_resources/other/circular_location.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-FileCopyrightText: 2021 Andy Scherzinger +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/circular_lock.svg b/drawable_resources/other/circular_lock.svg new file mode 100644 index 0000000..5b6959b --- /dev/null +++ b/drawable_resources/other/circular_lock.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/drawable_resources/other/circular_lock.svg.license b/drawable_resources/other/circular_lock.svg.license new file mode 100644 index 0000000..f496122 --- /dev/null +++ b/drawable_resources/other/circular_lock.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-FileCopyrightText: 2021 Andy Scherzinger +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/circular_mail.svg b/drawable_resources/other/circular_mail.svg new file mode 100644 index 0000000..63e268a --- /dev/null +++ b/drawable_resources/other/circular_mail.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/drawable_resources/other/circular_mail.svg.license b/drawable_resources/other/circular_mail.svg.license new file mode 100644 index 0000000..50a8f7e --- /dev/null +++ b/drawable_resources/other/circular_mail.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-FileCopyrightText: 2021-2024 Andy Scherzinger +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/file-icon-black-24h.svg b/drawable_resources/other/file-icon-black-24h.svg new file mode 100644 index 0000000..bdb01db --- /dev/null +++ b/drawable_resources/other/file-icon-black-24h.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/drawable_resources/other/file-icon-black-24h.svg.license b/drawable_resources/other/file-icon-black-24h.svg.license new file mode 100644 index 0000000..7e8e3da --- /dev/null +++ b/drawable_resources/other/file-icon-black-24h.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/file-icon.svg b/drawable_resources/other/file-icon.svg new file mode 100644 index 0000000..e610dac --- /dev/null +++ b/drawable_resources/other/file-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/drawable_resources/other/file-icon.svg.license b/drawable_resources/other/file-icon.svg.license new file mode 100644 index 0000000..7e8e3da --- /dev/null +++ b/drawable_resources/other/file-icon.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/file-password-request.svg b/drawable_resources/other/file-password-request.svg new file mode 100644 index 0000000..917f9bb --- /dev/null +++ b/drawable_resources/other/file-password-request.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/drawable_resources/other/file-password-request.svg.license b/drawable_resources/other/file-password-request.svg.license new file mode 100644 index 0000000..7e8e3da --- /dev/null +++ b/drawable_resources/other/file-password-request.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/ic_crop_16_9.svg b/drawable_resources/other/ic_crop_16_9.svg new file mode 100644 index 0000000..f1836dd --- /dev/null +++ b/drawable_resources/other/ic_crop_16_9.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/drawable_resources/other/ic_crop_16_9.svg.license b/drawable_resources/other/ic_crop_16_9.svg.license new file mode 100644 index 0000000..f496122 --- /dev/null +++ b/drawable_resources/other/ic_crop_16_9.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-FileCopyrightText: 2021 Andy Scherzinger +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/ic_crop_4_3.svg b/drawable_resources/other/ic_crop_4_3.svg new file mode 100644 index 0000000..08f6b7a --- /dev/null +++ b/drawable_resources/other/ic_crop_4_3.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/drawable_resources/other/ic_crop_4_3.svg.license b/drawable_resources/other/ic_crop_4_3.svg.license new file mode 100644 index 0000000..f496122 --- /dev/null +++ b/drawable_resources/other/ic_crop_4_3.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-FileCopyrightText: 2021 Andy Scherzinger +SPDX-License-Identifier: Apache-2.0 diff --git a/drawable_resources/other/ic_low_quality.svg b/drawable_resources/other/ic_low_quality.svg new file mode 100644 index 0000000..b099220 --- /dev/null +++ b/drawable_resources/other/ic_low_quality.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/drawable_resources/other/ic_low_quality.svg.license b/drawable_resources/other/ic_low_quality.svg.license new file mode 100644 index 0000000..f496122 --- /dev/null +++ b/drawable_resources/other/ic_low_quality.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2018-2024 Google LLC +SPDX-FileCopyrightText: 2021 Andy Scherzinger +SPDX-License-Identifier: Apache-2.0 diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..fc1ca1e --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +json_key_file "~/.gradle/fastlane.json" +package_name "com.nextcloud.talk2" diff --git a/fastlane/Appfile.license b/fastlane/Appfile.license new file mode 100644 index 0000000..3a4e670 --- /dev/null +++ b/fastlane/Appfile.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..3d3d92a --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later + +# This is the minimum version number required. +fastlane_version "2.58.0" + +skip_docs + +## public lanes + +desc "Upload Alpha version to play store" +lane :uploadAlphaToPlayStore do |options| + upload_to_play_store( + skip_upload_images: true, + skip_upload_aab: true, + skip_upload_changelogs: true, + skip_upload_metadata: true, + skip_upload_screenshots: true, + track: 'alpha', + apk: "/home/androiddaily/apks-talk/android-talk-" + options[:version] + ".apk", + ) +end diff --git a/fastlane/metadata/android/cs-CZ/full_description.txt b/fastlane/metadata/android/cs-CZ/full_description.txt new file mode 100644 index 0000000..41eead6 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/full_description.txt @@ -0,0 +1,25 @@ +Použijte Nextcloud Talk pro zvukové či videohovory dvou osob či skupin, vytvářejte nebo se připojujte k webovým konferencím a posílejte zprávy na chatu. Veškerá komunikace je zcela šifrována a prostředníkem je váš vlastní server, díky čemuž poskytuje nejvyšší možnou úroveň soukromí. + +Nextcloud Talk je snadno pužitelný a vždy bude zcela zdarma! + +Nextcloud Talk podporuje: +* HD (H.265) audio/video hovory +* Skupinové a dvoustranné hovory +* Webináře & veřejné webové porady +* Individuální a skupinový chat +* Snadné sdílení obrazovky +* Mobilní aplikace pro Android a iOS +* Mobilní volání & chat push oznamování (pouze pokud je instalováno z Google Play) +* Napojení na Nextcloud Soubory a Nextcloud Groupware +* Zcela ve vlastní režii, zcela open source +* Volání šifrovaná mezi volajícími +* Škálování na miliony uživatelů +* SIP brána: volejte telefonem + +Aplikace Nextcloud Talk vyžaduje k fungování Nextcloud Talk server. Nextcloud je soukromá, samohostovaná platforma pro synchronizaci souborů a komunikaci, navržená tak, abyste získali zpět vládu nad svými daty. Funguje na serveru, který si zvolíte, ať už doma, u poskytovatele služeb nebo u vás ve firmě, a zpřístupňuje vám dokumenty, kalendáře, kontakty, e-mail a ostatní data. Sdílet je možné dokonce napříč různými instancemi Nextcloud serveru a pracovat společně na dokumentech. Nextcloud je zcela open source, díky čemuž je možné ho rozšířit pro vaše potřeby, podílet se na jeho vývoji nebo „jen“ prověřit to, co slibujeme. + +Nextcloud každý den používá miliony uživatelů na celém světě, v práci i doma. Firemní uživatelé se mohou spolehnout na profesionální podporu Nextcloud GmbH, což jim zajišťuje že mají zcela podporovanou, pro produkční nasazení připravenou platformu pro produktivitu a spolupráci, která je zcela pod kontrolou jejich IT oddělení. + +Více se dozvíte na https://nextcloud.com/talk + +Nextcloud naleznete na https://nextcloud.com diff --git a/fastlane/metadata/android/cs-CZ/short_description.txt b/fastlane/metadata/android/cs-CZ/short_description.txt new file mode 100644 index 0000000..98b96f5 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/short_description.txt @@ -0,0 +1 @@ +Mějte soukromé videohovory a chat pomocí vlastního serveru. diff --git a/fastlane/metadata/android/cs-CZ/title.txt b/fastlane/metadata/android/cs-CZ/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt new file mode 100644 index 0000000..0a702a8 --- /dev/null +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -0,0 +1,25 @@ +Verwenden Sie Nextcloud Talk, um Einzel- oder Gruppen-Audio- oder Videoanrufe zu führen, Webkonferenzen zu erstellen oder daran teilzunehmen und Chatnachrichten zu senden. Die gesamte Kommunikation wird vollständig verschlüsselt und von Ihrem eigenen Server vermittelt, um ein höchstmögliches Maß an Privatsphäre zu gewährleisten. + +Nextcloud Talk ist einfach zu bedienen und wird immer kostenlos sein! + +Nextcloud Talk unterstützt: +* HD (H.265) Audio- / Videoanrufe +* Gruppen- und Einzelanrufe +* Webinare& öffentliche Web-Meetings +* Einzel- und Gruppenchat +* Einfache Bildschirmfreigabe +* Mobile Apps für Android und iOS +* Mobile Anrufe& Chat-Push-Benachrichtigungen (nur bei Installation von Google Play) +* Integration in Nextcloud-Dateien und Nextcloud Groupware +* Vollständig on-premise, 100% Open Source +* Ende-zu-Ende verschlüsselte Anrufe +* Skalierung für Millionen von Benutzern +* SIP-Gate: per Telefon einwählen + +Die App Nextcloud Talk benötigt einen Nextcloud Talk-Server, um zu funktionieren. Nextcloud ist eine private, selbst gehostete Dateisynchronisations- und Kommunikationsplattform, die Ihnen die Kontrolle über Ihre Daten zurückgibt. Es läuft auf einem Server Ihrer Wahl, sei es zu Hause, bei einem Dienstanbieter oder in Ihrem Unternehmen, und gibt Ihnen Zugriff auf Ihre Dokumente, Kalender, Kontakte, E-Mails und andere Daten. Sie können mit anderen sogar über verschiedene Nextcloud-Server teilen und gemeinsam an Dokumenten arbeiten. Nextcloud ist eine Open-Source-Lösung, die Ihnen die Möglichkeit bietet, sie für Ihre eigenen Zwecke zu erweitern, an deren Entwicklung teilzunehmen oder einfach zu überprüfen, ob sie das tun, was wir versprechen. + +Millionen von Benutzern nutzen Nextcloud täglich in Unternehmen und zu Hause auf der ganzen Welt. Geschäftsanwender verlassen sich auf die professionelle Unterstützung der Nextcloud GmbH und stellen sicher, dass sie über eine vollständig unterstützte, unternehmensbereite Plattform für Produktivität und Zusammenarbeit verfügen, die vollständig von ihrer IT-Abteilung gesteuert wird. + +Weitere Informationen erhalten Sie unter https://nextcloud.com/talk + +Finden Sie Nextcloud auf https://nextcloud.com diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt new file mode 100644 index 0000000..228f792 --- /dev/null +++ b/fastlane/metadata/android/de-DE/short_description.txt @@ -0,0 +1 @@ +Nutzen Sie private Videoanrufe und den Chat durch die Verwendung eines eigenen S diff --git a/fastlane/metadata/android/de-DE/title.txt b/fastlane/metadata/android/de-DE/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/de-DE/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/el-GR/short_description.txt b/fastlane/metadata/android/el-GR/short_description.txt new file mode 100644 index 0000000..a8cc02c --- /dev/null +++ b/fastlane/metadata/android/el-GR/short_description.txt @@ -0,0 +1 @@ +Ιδιωτικές κλήσεις βίντεο και συνομιλίας με την χρήση δικό σας διακομιστή. diff --git a/fastlane/metadata/android/el-GR/title.txt b/fastlane/metadata/android/el-GR/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/el-GR/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 0000000..677e37a --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,25 @@ +Use Nextcloud Talk to have one-on-one or group audio or video calls, create or join web conferences and send chat messages. All communication is fully encrypted and mediated by your own server, providing the highest degree of privacy possible. + +Nextcloud Talk is easy to use and will always be completely free! + +Nextcloud Talk supports: +* HD (H.265) audio/video calls +* Group and one-on-one calls +* Webinars & public web meetings +* Individual and group chat +* Easy screen sharing +* Mobile apps for Android and iOS +* Mobile call & chat push notifications (only if installed from Google Play) +* Integration in Nextcloud Files and Nextcloud Groupware +* Fully on-premise, completely open source +* End-to-end Encrypted calls +* Scaling to millions of users +* SIP gate: dial in by phone + +The Nextcloud Talk app requires a Nextcloud Talk server to function. Nextcloud is a private, self-hosted file sync and communication platform, designed to put you back in control over your data. It runs on a server of your choice, be it at home, at a service provider or in your enterprise, and gives you access to your documents, calendars, contacts, email and other data. You can share with others even across different Nextcloud servers and work together on documents. Nextcloud is fully open source, giving you the option to extend them for your own use, participate in their development or simply verify they do what we promise. + +Millions of users use Nextcloud daily at businesses and homes around the world. Business users rely on the professional support of Nextcloud GmbH, making sure they have a fully supported, enterprise-ready platform for productivity and collaboration, fully under control of their IT department. + +Learn more on https://nextcloud.com/talk + +Find Nextcloud on https://nextcloud.com diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png new file mode 100644 index 0000000000000000000000000000000000000000..5c75cc7cbf5d6f2d6f50618a06f520d8e9556b4e GIT binary patch literal 434972 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

>N@q(ypzoE@n9C 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

_r1Cw7vL&Kq1y+x zre(EuYKiooWD<>C(CvGE_OpNF{qMfu)vMP<(a!WDys-dMF=bK^#5-c2X-T~O;U{Dd z;=xoE)+KH~e8A?C>@g>GcOJTRk3}{F`re7pa!OP*V{yPz3p!UN3 zUB_;lxLJ7mUhviM{L_}5Dv?K}{G5gLA!|6G%6~rYpqd5aAaE2!VB$kvUwvkBC z<;^8(YpKO>pL#+L#UrVBdq_f3)Zidv6d**2H;?akc<#qQ91WdQu})7+x`-Y2<@tOw zWnO8>!yw~p2_a!(F4;yjOh^;Aq~mjIN9f(l{AAQmL3?WV3W+@*f2^b zKQo0|^@U@q!NFiOd>-mP^p51q`p1brFz?;Rl;U&p=OqCsJ+7~gj-QjS=eL^K*QOO@ zVKR%)J>LXCm^VO9ykI6JJ<>m!%hx}lxGWDVpx?3XuVtn`Ps-T_J*Htka``i=JCV;1 z|9;v{=7=4bNA=Cg{CVaJ(eylD9{`ZKMtV}_8|s!_b4Td8V@?1F8dh%ou*T16$ z?^J%Blq4Sq0_0pax11jkLJS9vj6CGreePX~*$l|(5#Hy=#@5MUIkUlBD$i?w7R~V; z0V9rREO(ndZI@Wp6)8u=IaW6}{NP7_gf*7m{NfjU^^0Hf^Z)#}?7sLCX*;r5-tT*U z^X`^?HndS)?X9Sb`;d$+#t13mtl+&W{3dG*ei0+^R&%efR}JfzD^{=8T)+8*=G7}! zuivt~et}zDgYiJXh~^g0p{MV6q|lN>M+!YD>@j@{At)C>+d@_=wg9p3*tEsUEpp_x zC_-$AiI@^0_K+gpIu?~Dg~)!p!&nCi_S+r%{hrYGkYTx8;GJjJwK!K%FB&DnL-_)t zl=N#|g1*OCPY|%)VVox$i*XKd4&xOZz=~Rzy)GCI2wg|hcy^mT?Y=`q zDU|mln8pzJjEEn8=xK-IbBr*Zx z4PqQQX2=F>)ScLvjB`TYMV7u{)vO31aQp6_ejiybmw4|n2w%MW4Zr;5FWGE1yjWkc z@P@{jg9Tm|OPYEK?GCIEjVfkDu~~@=cYgdIaYX{*!AyMnO8c z-!rd2K|H?U^PhM6nfskye7ZrM+rYog@{y3-bIOxe-|0!0Ea3C;GT)Sc-^=OyJqMvY z=Q*a==bX5lcJq1Vsi8y-?ubg*|x3ptkxBO{F5K>>FXE#{HH(V zx1WE`SHJibx4-&=yx$W-;@yWkHoHz!Mkc{mUIp8n)ta55?^{ynv63-Ztbj9uclO|g z<-Nyy%c7}SEE`s#-fiB>4qU!@; zI{*Ta(Dis@5HZ9MNih;*hY=2hSp&=eA#GDim@GJ~f`tk8n>{%tVu*w;()W@5en-ri zda=YftAhBV=EcivVu*Bohb=Mn*$Cb`LJUX_N^{*;wJQCt!g#L=yR~@l$V0Y< zlHx0-j3kY*kA067!}fl|{bobkws`B+!WCnQfj4Bsf#u(O4)aJreS+` zPYe;N(K;v)2c_D24FT}#Ef8YDScjxSAh~aO|KXN*@7^Ou2%UywIOn)|@$wLAF)({) z$@r?qc(0xzS#M~q)pVG}pXjM>xgtu==)Hzz3}C<^qd~cL00PG9>(G}vzI0tn5sXP# zYp_;GNkJ&*3cGoXL?UF-{OzbB*QQ8zj!%F1Ltel6gsux%=jnR2#`Y;9)^KxkqhQL~sXR_Jkv z1K;5OuV6{}CCBmj6q#%|*JKcr@s**f9Ns%j-_xf+5>aAHG9(BCC_Uo9l432-sdx`D zHDM`0bg3bip zyyo7K)6Y}+W_+)6;0#P%_LF>^{yBf|r{f^QV_v8qQ%+v@<#9d+)t*-W%tK&SE_ufe z_xrZ>l|KLgzsKci3hsB`hM}C_l)pOwV1B=`koeHK-#!5F`&6b2*4G(3;|<`SL*G3P z06eR_ugFd^S}Eq{NiW7VDs*l zU;O94rH-O$q>QCcuO>Ul=oA#rvWe+7|+LX@&f=Mq6*`{<1ki))k=c8#9q@nHon%d3umxJ zv9^^%&=3PxdBqdZd0?$$v0PI(iMDO&`<{AHv0SV`gr;dUg<{Tx43aa^WL#a-tX6El z%=95*JQ(9}nc|^07s@Y}LGceE92EVzm%w4Io_#-nDT7tedj$(Z(EjVXKnw~*8KZn2 z*#IV!jSy2NAVdr=KmC+H`jbD!*oysr4>+6?+8BV$anwCa*LOt$ z6ZZQS+j#bE$8xzO2DLu+As({JJiP8@GMa9%?_s-X$;q%<-B354-M&4w=*`OI{cY+Z zKKFz>=YKm`l2o?7NHX_NXNU0}Z1^85Ebk8xpeX@foQLO4c^$L^)$bN@M&r@Wu<^0}Tp zU!Hls2ukWDnc8_m+tSpY<-#MC;l;}})@Azrj$i)#=iGkzj^BRvYhKhVb|3C&LuQ*J zn|)8$Cz4V6@EAJ!kclB8qLw3z=4mEH8dp(OjvNA&_bk>+WyQBbv#eMw8eY7*;^y@W z);Bj)i-o2LbAq!%CeZgCvE5-pPuSe63w0ORy?;-8cZbcHT7={)QV6sgCB^ebt*bdi zQVePtE^2_cmJk9dL}ifgBe9Eg`;M;b$dr8a*5a!Q?=9X}xDwNDYKN&S+7w96VMXn@ z)>$g63VrMYZPy{TC&r9eO*v=1)qLs377{eQu9yl{VfB`j0^U1P5}X)}tJt?2+O|6o z5IU#v%`q!;euk1#P+i?!RpGpb7vzD=D;FMxoUmePw_9w^RMzpE-+qDjp5=0-Zs1j= zZt7Kqca^3NONnN=q%U3qo86w<&5q^OHO=*!Y^^Hfs1R$dQ_GgMh*J*>5+u>g2QXO4 zArDH^kd{wSazty@>qL%j^20i0tyRYQoYftFU=A)V?Ik*pQzSbObewXLU&au3{VI!7QQgWK28A<_1 z<`(?n-*ct(;2M2kz;}o9=GN4)Fb~Ij3}As?Up(rICDI5QzG8+@1ECV-lt@4rl&q@a z`~Tga&|KYU+Q&YjI%Y-Kb=b<|>k3zUu$gAL#8$<-qACQKl_!Tp=p)g=j6CKC<2Sgw*K#Nb#8RZ>B^J@88H};QtaU zyS9m=XJPp@c>Ns607v5*j$?H2BoaZ_%v?f$jE-IJ1Sf*zK#D@j!x-25NSaw$NAn|2 zI02YkH~MA@0G%8?{vPV2DDILn&ZNU!KYI8WY~d>zyuS4Lg`5YCdU$R*?62pP@&Vi* z=Jt_1@!|52Ogvv_=I0FKVtig2c_2KcJSMw**YflHZX$UC>dA4?>iPLHXTvn{Fpf{dP*R$Do7++(JW8bT5GziWlLf=z)i?@!P0(E6^ z1|;>=m1ntZSTq$lje&1gHP<&+T)kYgy1BwH7a)ch1GFtU1X3UAcYDHaOWN(}+Z}P= zLf;d18**srSJ6MKIs2sw0E#uL_HSx*Oa0+hDNb)JW$ch#NZr8L50s)pfhi`6 zfOTGLs_!&kKh&sevujm=$cRR^ox=xDNtqAde!v!JeZz;_-(l*#8e?eM7OY{+2`i2v zYPrSz)isE4?0UR++}}Uo+LoL5?=fvlE`oDjnI=|C^vzCiDWsyZWfR4}OdyC?6C~xV z5hX#t6BRYQ#FCjlb5`{Pmd^i7&#p$Zt3Zv`hu?F&y;DiC7_li)l2X%M6NoWZ&_eT- zbA6r#QKo{^l7N{>(Oa1sb2W)tcYbnsiZcwui1(X}BytPq zb$D69VoDuZfdCuI?zHdp6Qi;r^orHFa?X&&P|657*IMfpN5qmb{Nk_viZ{Rf1^361 zen=ElFm{LujItqo8`RmiT1CJ&4Q1%@Z9|NaRO`YUbpBbZIu70G$ldJ&*7!<~K0o*A z-)W9W)oKv2nlgl=RN>_J@89zA_Esx-JmDB=6#8*Rh4Thl^T}Ig;wiLvSh!m`Ywska}`(f>`C5fkOB9f2K`~qb6 z2mvTBgM6>(j_n-3%v#zU{)O*<0sww$Kec~F?Ku!+`69o>2Kd(op!_Me)iV;oVmKW3 zl#*$iz^?J+A@c6+f$OUS|NM`?;r7!VjcaNBp1ZE&;l5++BgH|N3&jM6A(BZm;y)=; z+u-ol;Js2FgZGdU-aD=jdySws7V(z$YRBQ`K-jmqU@6v+jS$I{u0u{IBxTANF+(Ju zP7KHUDnbX8f|CNykWHq6;YD&@Q`xt9UH>w7tMqJe_RHJh2dWLaS2idx+kzi*L zy$}NJt|hds0w2~^M@St(<9!7aGzuTu9aapdhX+a#j8WsW6BYY2Rvqw3r60Qh0IH#9 ztN@waZl@7hYhcKvVN?JhCe0gnt~w8bafTug+EyL@8WGwb_ShQ1*0cjNM~eDWPj@`> zvp4Vg^v!P>x*l&WjZ>`v<19l?4C9D6RiQo{4tVeB`;IYXj@^Uiy}$p8>>OQ8qyoWM zobwvBHxBD2<9`z9GgYg=RE2Bi2$)8^s=k5|1XHU3Y)X@^9FNCif4OcUsj6Oq6+6t+i-P#P>`&3 zbD!>mXBY;J_>Ut&EP^49nf=v)w%ze5-tzEpRG`Z`T^lwI%mf0Y6tS(xIbWmIj&Vp7 z3QZeS*C1U2cIV~sg~jx8#O(~QycATZ(d+LyYWLZ#VhjZ#iDfK>k9YSB<6EN7q-e1& ztj5(g6@Uw4^AFXO`mbWMNTEo}pX9qdFS}COCLmWl697>F0F` z=(!XaSzO`zyZS%frYQ5vEQ2HpgyeZ+RV7F>B&i`X)UZ9v20JB_wUTUAyUtiWL`uGF3=SK76q5e`Z^fgC+J+$#d%)Org zfS=k=?O#c|6is_-n@a$|PwmIu9>0JKK6ru)j6=s&6I2Aw2HMthyt}7J;c9nHj>7ML z_koWe9}pYJwxL+9J3pq3SVwSH^N%Y7Km>NdP8O=4uwD>K`7U8<0~^e}80nxF@9%F{TR4Y03e%5OY$BYfSXVQ>_31 zRs<_DJAG5X48uToIsqDyYnldQ9rve%^`3p(AkJcgI_l1Q#0QGCWMisfmk|ukSgbfA z1sfV{2#^#Ah-O4`CXSI(GS*p~_Y`BatXc&Dr2cLf3;;FeG>0^$$no|Ld+Hdv9&*tf zbJXFq&TFLH8Jf1yQftQGf~RR3#uy1rqiG4XrnVV{{tQzUEp-nx^H=n>XyPujzm+245ZjRGkJ}pyWxEN>uZ}SXKV7 z%%M~=QWPAZ=7g(VTqEU4DYH|w`>~^(S6a zxKeX9YYDGZk87MG=0ejn48s65H@=jDv6fsQ&yhEof6h@^i*cT?Qx-|=JIt7&Xv&Cl zjwER%z_x7Dakoy{c8$IS2!H}9TL56&e^aEQ-~KIO zx8ttsIE|4sW^z`20M%O9(e4_quCFP!s1AiJXv!6sDL_ArG*-xDn$Tj6Bc)6~4C=^< z%oP5uQ<74m?>qLbr)}HHJn`&yEx|V!15Fc%r|}ubCVvMtKy7C_v z0GQ&}1qJf5v3$CH;Nf`WDp*SPD;CL;J(IJ}%`E`11)+Y(rhUi^#OOFR_zXT|LKJpw zLmPU=p(yhUQ>zb6b7|4{=oV3)sbXsety{>v^d?4*Z9+6DgHH6?vWYRD&j6Dyh zqdIUHOoGIyf@8U8#ep&jFTvQ4^y2`?EOJ*2BJJf8PBe|urH7o@?OME7vA@s;Foq!w81L|{CyoP73|0*N?Hw*%2xWn3zZUG8M&!o zip6?|U@&6U@vqL4b-q%+^?3=p=9RkMh_M(mnJA_{)9DSXKTCE1F#qkGLwN*B%QaPO z(Ki9{j??joiy3Q$GU}W&VircfUT+*n_Pd=j31nS~z*>W#B6>L|1z;*bARy%xnDXHH6lJa2+M7!DD`sR2_T{9e9?Xa zXLKBi6|!g*ikKqNd4^Q@`mg_rcYpCKZu^l^EW<+wVe!L^6#MREzSjeY)%rKC;9w&|@THH;<%rBFI_GzJzN@7frR8$in zlNr-MA3LnAB6H%vsPJSaR}gPnM=J=zd0CbD{|dgGDZC4Hf3%VXD4>y-MoR_2s8L|u zXR7gT>OCkee-0T>=l)Li)+98x@bQt_13M$!guq?nxgUFq$+g?F z*4HRf`&_TrqVLKTjU63Rv;DpT0M>zzM-lIhy)}sy&XMb~z5j_?KYzgTt#5>?E1~w) zFiwJ;lQ1A!879^BMxb$y#s?&<=XlPUl+!#mmr?$ye|0X&s`Oy(h}oA}0U}%bQKXj2 zyxc!u-U?q<2GBeo;eTPeJ!VWi&w_n)|3y=XHXF^qz912J`5cSiKg*kWVNH1!0GOTG z&%B^B{pE@8UuZf3mmd^(&cT`;lg~W4HS!}aOor-HsHtD~cJCC&VMmh}qo z7%_aleGWqPeYWqL(lb+WpFSgBUgz`a5xNCnq|&lJ`^YZ=VCDO`{$;DVujezoYC(OP z>iE>>rPhX)`r@h?bfUS_rRWPHX0Syx;#vgBH8KD%E)eE>9>A~Zj~4;BOsf zB{YGyY4L4~YXa@f0e5IAjmI7i(6(SLrW7P4=sLQOANlZ4|BUnld*2XK;^B11-9w(0 z&-=cgDUUgil}c|g&Jg>^m=a?@5M!cOT8?auf@^i#%5_MBrro~{V zeDvy26eEcD;IzKIZ?8!uffaIhB3p+@Qaj2x{C=l~gVkr_oWlgCOaK9~npPk=B0wVs z6C79T8>gcHFTA&(<63^0uR z{=-Lt^O(ltulFQp>4%81u9gr~b_6NmO5xP^+}_>O?Dy<$ZZJNOt)@XZqZJLTaTudf zaWQ5t4;g%e^G-{$)%ULznNvto4dRzlbK8w*=tK-6Rz>Fk#8Q(6YKlS4d5>|9jxAeC zX;!J8aDU?*t_ip{Q2I_mBr#g8!5XY{V3Ywc#)z|yq3>(e2+NSt%HY*G&I}+afT8bv z5g}M;4ttEZ?0)$R`rrMIrU*@ep2$!NzB-%JVb4*0=1BA8+^^1C%0=4l(plRA0NZ@- zlC#<_6|s>FQrCIO1&br5f;25hdMU541memGpduYc`g$G zV5R_;6lKPM=1tFHZ=YcUmZ!@gLPj6j`o$$^3CbxVE}}h_OCp!d{ry1qM(6_UT)|pE zrl|E~1=Nza#k}gBySlnO9c?OySe8C7aL#9+#^wYBObiP#qWTV5pjey;Su#mfMZhoQ{Te*kOjTq1Nv78iN?Yj7nq#Kfp2OXf1FuW@jv^1jct zFAE}mnFply-*-~=5K%M~X%_grp$rtQAvCZN#mdqAv_&$vG9xx4RF0L?pc zqUPtGYU#TctWfMEGIXTtI-K*od2>U_g>L9EWW4iST^~4H@9~Xif3>IGwYbn=LxXQa zrJ3(l5Ud!QB)Be;yPo6yJ^k^Xd_2)uM|PR6JJF9FDaJVxqtynWaUL;BRkqnMjsxSE zIX-lZ!${5rXSLpVUgY#UujvTZ`|9i&unuD(#!;Qd$w(^&at0|Hp|558taErDR9w$_ z#0Un<5Qpjr>J!i9tlX@1=B-8>jnR^S6}a%;sZl#bE%9d!cOO2|pE_((5jIl|x|kA& zD)v`^X1~J(Hye7!>b-@g0TITb=X!Tw=zDHIeZq<1;o(7P@6ORQ?QDF95U}2pav_8U z=RHH;lTxDV2XvZQJt>}29)Y_mB|SAu7ZnZ7EzlF z|E-#TUO7OfZd+8fzDBYq0ALxRi7}FL0dqdD7-6^DaXOxqLc7S>2L%l%Al4E>pkx)f zgtNoFjR?;3Z(&=^j-mzcJz@=4H#dARhD51)WimVH%l(ZptFngMW3E<$&H1N$SVw7} zE)A9!2EEiVB(^}-bv|YT?T#)be*F*sz~SnKj}JXVKXCW)u8Nu!eACb#_V~8paC3tX z4Qc4HMsu)3(-3^%;r@YK3d0aFlGyEbRc`=Jr=Cxr?)mibmKdipof*j)k~*#vQdCNw z|BW%xcLUygLQvf0?F;?g_!Da!t`U$;M(n#}v1F^ARj`%VDx^X+^+ zRdjB7z5+!ocMlJA!y97FCAU@?NC3P*L%%r}NU5eIRbX;^%@@Ypm$WToVA-cdmNzf~ zWmUA(IikxLlW7`9b8?)9w6;j(Y^#*Ip1*X>UlPH-wC^QK{!`;|yZ`hY&&SufGV26X zt(0lsm=k#{`kt>9DjtDTi|eq~sW9yVKzwR_d+L6l+vfZCC;rsikI!EILI7Zm(9E=v z#s06o-_-z==gjBNyTaAh7XW~l2I+HGU`25C5;XdM1{5Yu&y~s~tSKO+ke+=5JkLPb z9{+RNYe0Z6oxqAbKZ~I7hXeq=UwYDX{2z9a`(rPn3P?}UrJok`*R<-wJioyTKYd|6 zs!^%)4y6K)Q*OE{;yY__CUey|h6G=~eaE-o{D$j&OWW=^T)pAb$8Q;oLaCgeo>v3S+Ovw{xEBYt0!w9v^7ihNkh9lJKp=I>X`W!0u{KXjKTzwL45`aMdsm zMyc@EVMps4hPx9Fzx}}7{Vf(4`;HPLaU2->PE+_}jk=0ZlCl8u5J4bEWdg(@Vy(xD z!8R?3A*Vv|j?lLF;IZDbN>JzN9+fRtz}Dcsz3V@fK4Gag6xj8RJMd4mj_z z-l?WQWiw@JB-mQK^BMs%mQ2Bi1}la*MsiBrbw_f}T)(*?#mH{ICtFcXhdRfbro}ls z3r*eK-QnAow_kmw4m*NahYevaV_53FO2xNR$yn>Kwd7w$-&3ZFDW;Tk!|Lo)pTXqh zPV0@%iL=W86b-+m+P;i>gO&-*ITK4#B4H`S81>m&)uuS!Js^Oq;)gzX73-6lrcjwC zlc8&C8Oha@+&C_4im<8F^u*1(ci2W9<4jRS@vKhHa^8!a)%~4wmFe;{3t)ThB{N`R zvn->?ORDwC@iIcGWeO#$^N_4(D28ABo4@2Q{`znE{X@sljf~yM{oOr{_1LDCQgoj7 za7DX6kbqdgg$CoaT%#33)9g6qLc2eZQ<^JGh*)wGy3@%0eMcMifjb^Lvac$T_7wP3=(`KMc8?kN2W4 zw7I^mX30x^@l*TpwJ!ny)*73S9X?^jKzXq#Lb>>xwfTHr(kg|gn!eAq=N#H8svxUK z?wWqHq%cppLW={Ojk+Q;*+ZzvuD*fRHzeodB$*o*5gTh{j;yJC9!WB_U(>$Z{G8p-~5j5 z_`vaWVjL4gKajIVR@!!l_nwlOjo@4bw@OJ^YpNKN;DfDckjHy^nB-M6HNPK_T5xnxR-7%O;h)zM$j7RPZ^W7RpVRVUrK zK*{Qqi?P@caK2SBJpmH}*eMl5FxKHhRiQVAoFS)DMaQaIzIBLIntrKHZ$UwbSe;MT zoE>=U9WHsus^l!z5L2XS+m+*CjL}^A8o|OSWz~WL0tG(Z-jlk4JPeRCLs3yQV=e7| zr=<$LvKtab74?lF1WyP7Kr03q!8yyv-+#;Lc%mPAx~{{!>g+ZRMPM99?KkJ}O{1)d ztj_YO>$TM1yLVunro5r~`qn7hVCn-ZCAyeNP{f=;kZCmADaSr5U;wyHRBQLqs4*FoEXPZ0g^_+B`F2#9o~B=g@^llecsN|_x)xr zuARuOW0`ZN>qpYqgL8-vq@h>C9j$?0%sMg%tQ`2I(YTN-Tpwaf(@Si^$p@Z$J3D{LK7N% z@Z^*@9#6HZ!hpAq{r(F26XV$9Lr{R~w&U*h$T%j7q!}zTMS&DjqNFra`lYUi0fz=d}xC$i>j7$lYn+>#{$mKV_w)n+3>JO4+>s&wa54n=YSUZ*fXDxdf(6pw_hC zvgUoMdFsn&H|?kFAFytu35H#8G@|Qt5<@Pz((B9WnXc!|%MtFE9P*diQ}*7nPeoMJ zZ~NJDyX`fJD0rCady|-FHk4QuR-K>!1)wG8TH>=ZWS+Y2OAMH&+Dib&Yi+SVH2~pr z7!yCX_Phw-CPD@4hq%p=-ukoRt-QKD zzP}$90C;IUX=$qEd}cKtblq4TaCIyrGS`x>2cK!XrD*(14ES1~+iHR%*KOeIn;j)~ zFbc*y+7Q?^EtEn^8Rr}95S8U_RJr7ExKet15&C|t(KM~oonk_as-R!r?D5`Xtd^YH z9d@+)9lJwIvv1h#TP*?RTVfVW5kkA8#KgD1|CW#6e$eP@EbOe~dcPx67{{I%6T{FG zs#aSVV#qEP$>bNTuLHz#k4|v52j;9_Eb}x0&Uwe^aIX2x^ZL}210DA zmO&L3jH=1toSP%^m(D%s?k;kdXXj81PApiDX?KLHJ;VJcd@gvj?A|iqG2MFzVU-Fo zr8+!WuYR-te4X-`W<1@;eEnh|CrZ+saWNLMI>)Cd{Q2Mgn%!4Fkay}j2|8#z0vQ@->phE}#Arn7T7rG`v> zz6E8r+b{cd{tf}OoTF4nc81HRV9;itrGt0r^Gj{(AfB&H&s!96z=$Q2NLcQU9WkHT z5IGf6EeSb~V-zI11R^~-<`$n%*K_`^tX+!Pj(FUsGT9a745n?tuv|t>yq zo?_L+5fWzr16a);4PX@2!Fa;ierFe*?Y5qey;Xg*Ff7!mc44^C)(8cnmf0tJR z0I)(u%j)RUIqHVrQ@_gMhYg^i%gDN}=5_#^it3sP2r%a=0Gld2(@m)nR-njP(e~`x zFK=waKqza_I88V3Q~Oc0KW?=7y8s-|H+^R>0091V4YwH>eCqor45FzSBiMPtS{_xK z<+0)O?Dd#Qh;agA)VM?hXDr^TLA-N9Foi>Kl(DCe18v(9aG>3{gf`IZ0!)r{Xk6W7&lGBb-Uxxv~;H>SE&Wl8kefzpNKGy zBfH%W>gUV(x&%y@(P3i@24$FFREwfHTydK+dokoh1zy%=)wai59y7)`J8av?>2m#; z?nlS@S`;p_R=PLLG+h*giFW&t*=XuPXo$+%&Y@OTb3g-Tt ze>nfX321%RJuT;n0pnFu=Fm1=?-Hl}o)~ilwaW_Mxdi4cpXF0bj@Mk;Epc#jj#FhyoN9)mE%&3 zz8pn#k_vNv+oM)Czc0`KugM$x6KStSOn=DpfBEq@QKRy&3<8iB5?G!=w*jmHE9XAj zem{YR=l#uEW&F{}*C`p(Vlb6DDk;%=hclUd@rcB@W6ojtpan|zo z?Hm5`FaL_;LpRqO9)^)|hzwD$$vIDGEY~+XuC7{)E$sGM>)SON@t*(y-*|k`(t550 zBxZj9+iy6Yj$9oMq>^#obMxj6&KT})Z~6G?Tl(&V^^Ugjcxy>s$Eor7tvlL|thzK?%#;#Wlhf)aE z(Z>a=tslvUv7cihC9SUM<^(>_JVC+B?n-FOvgB&i~~ndVNdS>LuTaTw{U*kOz# zVg(lhwo;I-wT$D4b4~%8g$BTu0Wf_wNA1OGx`gxK18?5F7}5N~LIp2{D!Ke^xL5=6B!Vv*CaK&wu21 z-+W*kQq^71G?p0kUYv6b6|_=D`xI=_6wE1Y<>5i~NpdQy{+RB0t&i}yOxNTfEuZlc z7+5~@sb7~-@a5;9qmuqZHmTsVuwcz1q#`_YJ;zf|I!I+j*m-Wu_f?NGImXNWJPUBX zbp9m(U|3cyD31;97w%^}U%)yrW5F5W(6+RGTx$v#sKO-Yak?Iy^5R7-g=cFU;2kFNbdSjGIjVKyM88lr0kfUW_u+3$RL zGS_z(sda!CK@uZol>(qquUtir&hsx9ElJuoj-S6h;KmbIjpO#yN6o{vjx-DmU5`}f zuWbYGfBqI148OnqhSRYJ2jh^5xezlH$%F=+Gwcs7H#Y}dW7+LgRo^yRL%uoeXq$j@ zYDB6tT)6%4fzxeIqOiL@u)n@m`gkeCVdVDHJ>A0tV@g^vpaM=~$>2OBFr}9HlZ>gN zeFYVoOA(9>bNM=J3=u7lmr}xp0HsE+3ssOTBi2z;0W|l0GE#F+l@=M572w?L=*N^) z)m+XZe%NXR16eE%EmQT7(H!Ywh^bJDs%$&2z(_6yZ;YO2JRumyV@H2~OXDrMA2^N! zO%rfFoW*dhA&I^()S1<=k1629kYhx$rfb}O{K(Mv_~2>xJBkRt38bQqwD%709HdOw z4|HRsSjWw~x46TOD5{$vAz+&ZQ*>}9DzIH00OuSRs2oxSWa_u?weqMa>pjmGKf|w9>-_5Y#%@B61#gQ`&(wmV&Zf?SN_yGzI1+;tcT}l!q2UN$yh`jCPkW@^%>egHt=Wv;qSTr`se)S)KgNVe>jq( zI*P^xlCdOfXs@nlZ*I7IxCi5S^Y*}Qzpr`j6TQ0jkyxy6k(kNGL5YlgCg;TA`VDX1 zo%nqp30AL39h^yAG9x7;CE?K0wYd~kb~l=4WQt+z6UVz_71p#`$)q|~DJ31h6^PSR zqIB--a;B)8X^v&x-inq41u#{+Y2s~8=ARfL7InO!U}SB}*=cx`vU0qhYqI%$^>>*j zR=s?vItbJF5ipg?Ul&dhW$=tqcsPxeyhpOZ6w!(cSy?NoSbaN9HhF=PQhncNtxRIf zn(?q5w}mQ>y|A%oro_1v<)cSU@H_eMQBoG7%9Knih(%t=4wZ4;WYM`;2Dlb z8VqCCsUE<-;oZ-_#MIi=TIjS7n~YB*IVq@;|D37axLgH7tpk~N}A$rw~zFPDsO17d|7dm1t_jGR7v zK;oz=3nWUTHJyVsL@Oj~$hJns5u9-}&X9?i0C0n@$QeWtqdEE+Y>8wM9>&b|&wje)Z?S;{W+?|DAvQAOD{^qas*4-CM$#q9mXqq)LCD=9V%q zg)~I&Z;$%ys~FtEXHX4T*g=wA!^rz=ovG((KY~QZyPbSR`V-(;hQq)E!W2+sh=NFP#ZXZU5L5MyR zY;_ooC8q+;V6_|uSk+P}sRBA>(yx)}rF3x&N@3Ren8#(w#GHsZ>7u=0vs|)wrvGG0 zm91+}DH(&{F*FvUjidlqEqpo|N--qmxYr6Ll_gh?!+Z|&XzpF|Cze3nrR^?1Otp_i z9tQ`e{iQnL3P|h7&@!iHt{m}X@CT{!Mwx%4j&Hr@%<`0vY!DI31r3b(>x$eiLB~t> z>y`<#yuUALnW8CU>zjUE`w0bjwtZm=Ky_nfo==y*|I#)&%s(P$a%yMHc!9<-EzL4_ z@D$y%^8%{qi(>`|Q9xzMqbM(Q-(!bs>j1qpW1jPmM9x?8#1=0e|6f-7atHTMsZB4R zshpV#11-nQPpj**Z8rR#*+iZiQ5q%M8j7;oqCj!u9nv-N$ce!w$*dn?{*b zMhLqWceP{x=9>3!-|*p^Z|H_km?Y#e;sk7UK6lqG`gnL<_reX9IYG9@J}!s&RTdw4)Zh=bCcL)&T@ zLZhHWsg7D=NG*q$^xErqySu-qKXvT)J1~YZCVbnH3*0{(2~9&T&>c@qZ>nM( z?cs{u)ipE$6N1vOaoUeF0AN=4sih+G>~Lrud4siP8orOfv9$qdX3Fc-rfAa?;eN{C zRviThu2~7>O{oEM1hl@Bmr-&Nt#NLwRx)tTF%AQL-($Q3XTdm()iReNGM57!$8pZZ zPopYn8&SYPa+$%VjA(szXE3QCA+x`_ChT^Uy9WggYKqCyu~_!c^!v6imiIC_^Aja| z6+xBDEA)IEPykU1*;*n7+aCD$|K%STO~a`>5mTZ&b#z@1A~+W~jsxNx*Kgl&eSO34 zfBy};{T>ma9|oGHQRY%k^Ruv3Y|=Tkh?=G$#{s)K9O_gum0WNaC$iMoB#EX zGpaX%KkIv(oSyTToKrs*`Oee7+X&{w0+0%joW~5SKGal{1?_y{%qa*^R^wW)u`*TI zq`)rc1H5eBecx^2)aINyc0H#dvfp`PGI#?yr^*D#s=p!AcsakQR%7)!Cr=TFi-8Mc z3>bZm&r!~|`z?Q$`uyk8o?bZYcN|WI{;2&=U4OX%xqOZ__gui{InZu8{&u_PX!YCe zbbWkUGn8HYr)1>+lKiCc@an)9AT7;5gn9U^6nq+vi}LW)w~tt&`Q zs@AXkSB5dN-|uDvN~!||=y6LSG!5M_5L3d5I%~DZHS*{zR?I~MaA}N9w9Uz(m}@YL zA(lju3C`dyJ$>J^JM7u*cASO*A6g1RN|~nFVS@^>Y5dfXMR@z}J%=}MFioQXwDoxB z0af%jqB;zAVfg!?=?#;*|CE{{)?$n-`s8t*`_$5gtfplWooXH|0E1h!rIeYWFgc4kX9YcsnH`q8%G6* zTq(+;X?KjbQ)=PbVc)i;u-ukKH1)w&d*G#a@X}FL7EK|EFd9oIo*zflkCnO^r zzJABy>WX~oxeh!2um9u!q*xU;%?pQq0(z!CQE|{UNVL7zX)MjmjHP35pzyfD!*V;3 z>AI%}yLQw}6tYT3-OReKQtP#bqiF@LE&0&pro;jG2w!oCUY+sJ)?B4!~Kqb#lO&_~fBMYZ_sJIQoILucs!V*Bc9G@z#CJI>%X4|X+xwfqz2%zw6eIhm_Ty{rxk2^i z5ATHm`_jt_R!H`GV8Q1^V=pdDt4nwq;d}k|XU2n|bY54lRh|E{%Pa-FlxkS1+P@T~ zDCJUf%$5X-daANK)rI?ijsdW}-dtaBPq&vS)QN#o~p&N%^nvRpNm+g^Td zXQxObkH#5Hj@;}Hc!9AS*oRh~nv%f^fA$x@;+MbrGd|qi5_T=OcX#~LKm9K_K^`Fv z5oaCmzIw}>_itz#4{b}ff!%J0Ya5(*?Dl&t@nz0=quyBrOW$|wudX>94%Nu^*k+I0 zH<&hnQ$aN;7TFn$c)V}1RY89$dpJfdotM3(kTj})f25cJCzdQDB_?u;P$T=~OoSqIa+%>}cFVons*>FG@loAVxz zF!mj(rp%Zs{wD&?8l+_MI5OPbGaOIqh^2(Jmfc~`vG0f!vemMCswio3V)0^;EVX># zNE$}cIC4Cl2*I;I9JI>7ZVwpl?vJ!PWfVLdJ8bY+>*-VG>fL+7^$p276-soDVw{#! z6oVK)=ZVfVP?S;OtjrFu=BsK%JW&|N0aFUrg^Nz?lIAm$8*K^I&~w0 zw~nyebIOI76WJTYUVeu!Sqsa`1DCd2-q%V!{0W17-oFaI^%(}=aPy15;1_@MA2{JS zjU%USvxFuPV`Lmh+O}mJ zM*uA$Y77z#I8-|$G?=C(-Jcj!#5zyk9dZ7g{$Py3`-N(~RGpLyAkzehEYmBNpptH< zD(K5KdFjZ{easZ~r!t8qr%X$#E{{8p5z)nccA8CnS60gQBzXN?WEjqL?Bz3jsa!;=wiZotC|ZrV{)tvfXDXAQ}4USw((^)%PeBKd3i6L#^rjkb-d?s zEDAvCnw+sI<18H7!2UGwFk~p2=Al&(^2&Z*>w7v-ywR zH~C|@+JIT7NgH`G%vkOw#N2+JTA?*rD$fo=B!c9j9{!rQ0;sU zHEzyE#;b4Z(e-cd`RTu3f3olYCGdIR&&BhXY4-)ye|3m;6m@F=Q1dmV6n5V7cE2Nb zC%m!r{a6*+g`fTM7yQky|BkNj2(jgOchCKYkCZVI$HEvBVb^l?_KL56`4!DBfGybF z71?S1`KE1f!PB%Y#yL`{dESHO=O+x!^;@oQu1Ps-$u8eeyr+Z)X&Q>HD&R2_k0)}m zB#GoHqB`BaD^=M)lkFByzRa!VBj>&Jv;L`p4TD58}5 z+U+?drF+{d`c~AbwcZg+6{nhMSOw=)M9exnM-@S%BvPMW&Y58t$T>46y^f~Ua(?~L z;j)2}aYhyL^DrWn5@oc97cvy$LVwg;QHnU z6C5eV`E}9mb_^+E1$^5w#)xkm*Ke+nc25ZnrfDcM2oMl!5V2_KLpwXXV$ad;f|m8O zMbm_0$%N`0S?^|Q?v|2$evhslv&qpN$B_`iBZuu#v~3X?o{qV!cRh{~ku+b!(wQ(- zFtAHnv7yBaxNQnSYwO~!!SFb8g*=lIn7bfQZhMaN;Qh(Flt#zSL@N2 zjNk257?KHVIywaHa=eGS{ieV^)-Z;wshClnezvm>L zK4!+4I6a(*L&S*1*+5DKah|K2YudvV#R%T0)Cd%qMlz4U&5t}Shh`Te};o9P{* zV_Ud==;^cIqKa}_yKrPT=X-Qr@>u43ZDS}1upD>GIlbKXl0CKk>~TL?1y_y5+Jv{@ zZDHRy_Q7!MN8r46)t%L`MCJh7CBWpl{@vd1(;LtKySVSA&sW9;>>3c%I@ z`BQ79qPWw`d|TE)MQ8NIFVwoSPb$T`}2q1zrWUb5Qo zB9i;;9scP)o1fYb)0UvzGQ#y!`|_si$T@DXg(oND_Z;!lC_823?ado*4ts{<1LH8L zl7C73`Ct4Q|Nejb&m7*|@bKH7q3H7;iDHXLo48x0tXe zgrE+cv-q~H`MnXwLK-8<2!0o6+LqB;qGZGeBsAF2(%xJn!Beb|vO52SfC?NsrWoo( z8#fb}8VAY{5y0C9aZrdz0v!0(P=w`~yGnI==xgaHD1+Xeor{86)b>y0(?3}|b zjLbR8m{h<|KiB^iE*mD(zLVpTV%_ zBi4KTt^uzihu%45Dr8L?6I3tY`0&8(?JWRTH#ZvPieq(-EhQUL5kj-005K<;X2;>` znzGvyn-*yrBs3W7G0tPH1JUyqF$w}u0r;{QnWl=`RtmgShhS3UFSX3yGKYJmSf9_% z#x%CK2IbPRKbIb>&#Hm|lhHX0!~Fd8d^~BumQqyQQ9oqA-*b$aanO=~IVN0V=W}*l zr=pA@Fpi^w0x0mIjJ^6&pPenK6+jF)i?M>ka{c}--}nZJow7n;7CBs%s9VhWZH0jC zdLSaRF2TxC5H<3bZMpxD3&kH8lHuR|r~gXx{^y)xp?f&eJ=_wzo*WC#dPEF;9BFTE zc=xlPAtBJmM7z^6lKn7fRf&4N&RW{gV2$B$IPlxw{+4AGhf)e+9O8v^?D4?y!$07*71wvc zF<*g!%SE^-lQviguFyDx6BtsS74}~fh5p>A^}n>X9?R;jN^JB7S{`6Zps~*o~LKg|mBU2b-!HZ#cb3J5jS(wGRjkp7zVmB}IwG?(KcrN1)U_Wo zMHyE{Daijnd++`vIg;G@eLNljGPA0Cre|h%c5nGu?vfIZ`sZb)Gn$Mik`gH$kGi8s z8Aapq9wK!-F3Ek&Om|gfCJ+&>A3OpHWMx%#PtR+2JIt!9D-($f0P%4D`q#gn_anJR z?P4v_CkR1m6qP(HYpWKVggHW=bHSgjr7g17#Yk4As3Qf?>ZOQ-7(CuN#=~LG{dd*i zZh~}&!=AVu2wkK&MMJ0p%%yyxz2#7Rf& zBDq)zyv=sQ_48+h7&*p>A1wOsLWgs~stmY5o8CANi%902E1_D!5uF`-fK^Xa6FMY+4Z<@o9)B~3OTLSz~fILCYMzt7dP=S-p$ z$}Hg7QfeAdGi)~GDWRosdwV;Bb1R`jXDN^3Jvxs3GJDEV#d zwBO!Y3AH87c=^-DRbxslK@{tYjq4?2VhKn!75`HTfPp;A{+vhfm-*atxmIa<1G=s% z{B<`G69PO=lwGK6R*edm<}|z2{ed+AaK1l%k?ld|;swW2z;t1N-g~xvN7p6x$BY!2 z6BVEGG0wq`U#50`&BZ7-t2OAU6omEX*A*VlBm8jR|5*JkPZg4X*WR{02>?7q0jlqw zel4Qm%|>XoQd^|C1;{Hg36T>)y+u}*5wJ#I0;-~LrCaL!E?$|9<%d*nKW|fNxj}Tk zzGZW@V=8KOgGFe0D;~?AvNA|jaktn#ne#yrvkECS8X^?YT3oN@U#%tttRWNbIbaVy zqxt>iGqrwRK9X6S=)x8Fo8j7YT7dC6Mt1i57t;u=|R)RwcSRMKO(J23vLLVBn z+?1;ECnfcQ(iU7*6JHH6+IzWJ78f#%GVlI3>IOi$ekx&v=R!@AOkme4?wwo=j93I)6o6UBEk6RAMBMIf%ix-5xqlAd|J)!TB z*bzdk*Ve$4S<23$`zzG>ME_TX<2V7XM%}F(n#hULj-JMR(dLlKT$;-*0D$Irg%D;Z zwmDf#dVGsC$&9`$;-Cvg)z4F+>!OwV7BDN=8Uz06#Vv@;raz#5=5-@Brc%h!MS#a znRjDnHbApImg|hs><7(|h;6pQ`WG?A*m?tqiUsF95gYJ+Si^`>CH*XV>d} zVbjA^FB~SAu=Ftl<%+HO^`Z@gUFVcD#t^21HmIuNVgKR7La zq1vMWz(WIWe%}sS9DllLv&ae6>21TkF0c-<5>~3G-2OI^+W=cI1f1CBCC!OaQ-PM* z`IniF*UoqTKrda>Q}er(K)ZM^)RrJBGTaP!PtE0T28>!=QYlzoTaBogqOOv%G8HA6 ze}#%eDHL({I{0SG(Rv7%flM1St{`9)r3UN*|La~R!XU6rQ{pl+eM!32Bav#MUHtQZac2O z^&a2{pWd_T%7q}#fZtRAt7l!Z(R}d^4W5)8j0In?NGHv(1~+BW zXLrO5scIOM7{CjOLfmZ(py@jVh+S95ubR^Tt5>F>kTbjMYx>P#;%{R6i+4g4hdR$l z#tofSY}jl~;IHe@e!%w;U)c}NFQWfqpt{uP|9YIZfvQ#5%*JHz)mW4IcIYoDllXD_iMwlrHF`gW{>#Bu&`$}jiZ4h zDOw$gDbCqp*`kW6r%?94%n3QmRMI!fqpn;q#VuSi+(2wp;U zWt=%|OW@C0P@t_M&Y8W@)}OY%)I=3Kz5)@H>HwBxwwOAydQ>_lB8RZy*MH}C@Xwxc zyt`veN2bFac^WBM>H2{paF|B6&!6$($KNJy2eMOh*pykTq3J#Jea~jI;m1Gz2{A-+ zR&usy8=_|_iA-gMAf&q^|NOUq&!7D3KScH;`8ZkN#Ea=(sxvUfpR_Sdcu|0axK=m4 zRe(^GllZ3(LX4-+OInN>9BQV+gT|Ixm}Gl?xF2 zoOp$taHTNB%=N}|x1acI98nh$!W=-8k|_d9dChZ2`}5ces}zM51ahhW#{7NoTxWgy z(gBH!=PvindUL?cslR9Qo(`MYI8^(l?i-5(SZb6#t>Qrcr+?UAox+4ehifvLa$i z$(lLl!l=rmmf2|$|4Y!t7u0860vc<8;FmS;>8olLO#QiPZ6r3qoE6+7Eeg(TG`(wI zTvK#0UTR(@fGXYzAv!uIlr(}5xaf$!=f#H~^8Nq*x43?G%};*%V^S$xKYzx}XSYnH zuzhyT_3oOu*&xwsz`LP`*b#SIBzCA1x;V_h11Muk_}H=OIzkM#mZ_E0+g)Fq^Djcu zj1Li9pp+Bk`*=Lglxh>qiYCNYl+1)?M^+_g%V#dxD2U!0Z9c1-pkA8js^c2F-cl4Q z@auyk06qtz&n8p|xGL^dRGF$$df{RE?=lMA)XP`Vt@?lGJlzn<&OuI0*`mWv#6Sa8 ztEY^jIT4-b?&gMZ92w@E=~Eu}L}9qv;cLmcezQTGW1J?+n7F&SWjY+J`a&0t(V>NL zoai@$-K*f4O6FKHVY{K*qQpjG>5;svuB3=l3w1#(wgYm zlInko{-5?56H{Bz{lUEBru$Iy!zREOnoJkr7hYeAX|*Wr@n|JB2~}cG%^58|aL9u61HbYcKjedtzD3T3G$n3t z?>HWg91ce-^%){}$0Pk_!+Y<)0P!5g$+V2TpP3I0Tp7oa&1N&tbLo2yX+pK2!9(zn zGY%{9$Ns=)|Lvdn{r~BA@l&qgqB*4mtM4vZ>pXJ{N6^-!5CY06FMK&hz4x|0)U=v5 zzLrsJEoH9iu@v@f_t^wLTMEHC6~h^)A2hALd-+V9x3ryep|knf$x7kr3vo5x z?T<%}Mxz%`vU>q@B0f#iz2EkJtyF({wOKh@~os6P!9) zl?LxU+rhILGB5XgN)}_-RLA_wbfmw^_Ep;J=czToy|MjRv$xL!hU@+1v2u`?Se9RA z`^6X`kGHQ`1W;yrmx=LUXez*hP4G=^hN9{m*%e&?0IC#MBl-wg1urnw&n3-_s*F@7 zfH<-=N|zbjUq*l51_1a)Sv23ZFQzR405k5(H+#bVoHeE9SkAAQQ{^kwv=`yS_pbT& z`!DDee*91WM8DhdtzY>r&p&)&CA(rr?<4!0JCZ8f=g;W-4J8-i)fL@t$M)(9i5)q3 z!q6ijqT+BaP_jkDiYlS+*$f+thE~UaRw7q897g)hK!|-^8&4bdGWWXG=VHXG9ds7_{cT#NIRWD$NRkf$;O3;?F$ zomWmQFgp%#p|o0UQU`v{nUWG(1;kbkns<;hq>NzVeAE8G&1aukee~Kfqq_vDINsjb9-R`tD9&36$dnR;G?1ax z$m1z)G)KUkF*3gy}n`}0|jHVEF)(#Yw|S!fOZesy<6todT*j`MZx<> z(LyRh@jVlck3Rk$zw*Q1;#QU0{ek0g)7z$K8ViT$}MUb!6G%O4*dKCH_MI_FN67RssOLt87>l@9zp zo9diF_O&)KKJDQ+a(8!N+6B6TrAL%1qIw~ZyFHVpt#Tfnp64oD1Zcjn?mIzBn}2O> za(FKURbr?W0s0OS9OsC$v)rw3x`XjV`>KO==PZ)**WUCT=K#Q^^Q{=EXV=@(Q{T1E z-@XC>KtWGb7$;3s#S9E-xvp(7fw@fHn%>kL?fQaL%2eApAD9g1i|>=YJs)I`OtP7^ zAlAdCC~VQFnscXOlzwsM*EOP&8bji1y;FqqY8M&XA0r1NN0#rNo z^Ix;B7pTv(V1Jp5{A`%MW;>@yy<>|!-Y$)rH(QS!h()SYvz4;bLiCmw`+PU>;%dXI z&wffeP9!OO@U0K{-fw)Lu-ov{pZx>_4TRz9isF3DC-2b$!}T@N53t$bVuy>teuqc_ za;nj|YOD^?1Odf+qW28_04-I^)iQOl!+B59f)vZgJ&&-jRNgt!?B?j1mif4nP5iIq zf)}Clfg-|GYsRNyE9K7nTF1INR8B0K-O23CDKJhGIVFo2i4sCsMFqRA!#Ot#n=K=H zP2g@WB{#p9PkG?AbdnE&l19pu>>1>Q9*^vAZrI=6;!tunr{Bduo+c7XA0rA(C9@j_ zyz`Wt8DHJ9e|bYWPQ(z1LyrrdDOYEIyCEqYrUW6d+g{`P0SVFa{QCjlRi{5h2$t{f zTj@V}PMFloV|#O*){H4;e!Pu4)fvd_zi+j(0$XX_Go28JmxDHJ@(ISzQjvlo2lhri9} z1BY>B925Jy19{4L7w|rkMeu#k=IWZw?iz^^$(fWb^}~xd>UK>h?7EJ=@430TVYl0n zb2g>^X0&-B=ft+}nQm_3F!JyI@Q?ZDzx{h87djX0c{oQBV}^j@!ez#ZT|1USdNw|r zliKc2dwuDgx8nfvKn=fBN~Dx#)M=V*e_rFK=^7o(m z0nAAY<_EN+ot#qP+I8RCFLnLj(_FA?j8fQkp4|``?~(yvZMEbhwO#V>x_S% zwyNR+_tvGS0*Frm0OtVj>&%HStLd5j{)^bY5(a>xltu?ypxsg7yTxg$KjG@6RU(BNQrQDY% zHt5ItF&q1GiuAo}_uHFBJ}zx|&N-s9vPPal@V1tI>-}r;{+4t+f_L=8hVOp=SJ}P) z9*3O3dmNsAGoT8c^Yq;Y#N)ijZMV417WI)51KqHpIA5vznNdgjVB%z6gs$sQAcVj$ z47hqpuBmA&Oey13s}j9{${cZ9UXwZ0Q!PAgjx(Hr14YqXspPp>&NUFc$Hho0g;War z{T?3zgA<%8J_L&nx#rATzH6*`+(lbXtKup+u-R-VrOc84*5_%OX5+W@EhVG}yI$*kKBIx zlH=_iA~0NC5r&>|I^u$7v)huzL?XNG6>+mA^aBzCuIq?>PaFn_M)~)#182^xt0IaZ z;7sH&7wUHx6O}wOFwYIZmLhR!+?J9T$1fNJi}Ai>6P%Be5W<5|pd}k2r8HB$TPw5C zvHq@ho|F=yjw1*3U5^XOq3_w>-qBUir|bKe zv%5QP_Xqa-JzBu2BTWV055&Hw+idVLk_5DeZ{WXuTsk zk9vp13QWQ*}wlk`RhOX3(9e{=i#cBgm=W~p{D(`>pZW3GLHdHPD5iBwXxA6 z)pjq|)hKe#j9n;8xlK(~3^ zHm0jIc0m=yrbhSk=~*uI2ii|z4V=vPzOL^vICev1dkDNbB-^j*H40Rw)8p;yv}?>K zdfMLqDsARVd%?0j<)5q%W;IAJ?Jtj=yB_^o{e9rwCqMs--oEA(fJGF)I{XfEg4^Fe z50A%`Ofdz2UF7^@h-1=%Qxp6sA|%n1^Y5ybsoG{>*`kdiCRowy*X+jw6X#uf*S@S~ zHFlRmKi?n#=NsN$2MiPOmhbOCvLy_qG4bsWKjgX({Nx}0f#K?k$-#Gi^j*Chu9-x^xmKMkDNw@~XgB)ySTW_GQG@uAh~G*z$I#ab%h%@;Fjbu0IKKK~#y(lPKU% z&fneLEd{0940IyI>OfPYOd-ZOBC41}5WPnb?(Xhp=W!gzRYbb)dveYkk4JL`tLT|X z6*|;{^MRbI@mZ=2F)f+l?wTAq>M*`cjS^tc|s%_BdlZw*Bg-RoC*Jx!tLFj zn@>JvygM-Xz_8uW^*v)rWR#(c6@-Rdfm2ux~%j@mV@}OE9RNd&$UH zj;9v+H($QLErZr?)FU6gwLJjj?ZF(fxhqgqM+{rGUeuWPIP@x=7mJ;?c$ZtkpFAk~Y0Iw3m+XC4+hGggF!|gPBl96<&Pw zG2i{cZ}G{P`0QX~8s`|tiJTN4JG$MD&DE9lv6#@77l*h2Dg-YSDWq)aH=E6dX`1L4 zK#A1r_F~!{stUOfGyL8E^B?&4zyC+vefsLeq)-FnT=8`3h+;Yc53w8CwR#`%ew~&A z%#r*hkaRv?burG)(8}xZ3dQIC7XKRD{iLhl*o& z{S16S3T$>ehG$nKAJMMIi7DKRi&*An#^VUy(bZ^J2!YsjP^0?-mUmuj#%C?igw0e) zMe%|OhAkY%=H!?vCCDlm(4vYV1Op`+wK=zvcdqs^%{j#)j=t*<5=oCZ zC#2)Zcs!VDIE9=OiZfl9`q|KTC`#~lPjBz;*njehyU$+IInVQ}Yg`QMr-|fYyBTmI zO=Z5fXul7h5QFI>EPim_qI+#`5l2&2xAOJQ40E|m{cq2+IYyVF_b0>M zN_pi{i;5PVD_fMRRKFbe!0LRL4E|*lu{l?#y#KhIO||sb8o*dOG7Ype#)<94ec#jf z9aGMXl|n8FC!XNFIfo)_w_EzYp8>wRySsU8bP{m2^l>g3&4r)+=%4vt{?+et_v4@8 zR2iMICY>{lB(2X(i#gvc7y!))JUN39qMIG=P=zzwF-vWa^Lyr;TLAzq%H7sF%W=2T zjmyt0Q(a2QGaG0r5ZLa$upW0d)!G^%b$)$$0Khy?oIG&uS~?ZsIF8)z_dI_#&~!j9 zZRx9dqi+WHp928agZlp}-d7fLR5)o{Tx!kXH&D#jh(#6*85GgX5Chw;6<Y=8Ui}gFHzfomi=9JXxaCNqX3UhLP6hfj=tS;`NXI1^~HdYhfjBgP(S{* zz4i!Y>5F$_-|T#E`~Bw>$#X;Z-dK3z*|&MvG&$zOWif76&i{P|KzZal+*{u6^C;qZ zYh@U9j)bi9{>p{*;j3)t@rTt|gepE)pPO2V`!pT-;Ch3TL{1~ln_~8h?|jVr-~E_Z z~>`F3|)`w2XhodhlH*I07e5ZxgZF^d-|?psLruPx~!IR>t<6~ zN3}la$_7v+Ps!%4Gn(fqT4ZIoyecJSBq_N}c3!OXVNpog4D3uoF$X_S6H*E!B~%JK zPT0hODJPE8Xc2s83f|FqL;}HwIl^j@Er)ZdmdY}=!lAJloRx|)hqzd@c$}EVu|}of zm@?yT#0N)KqsbqS2heO~2y5Cv%BD3Co#QZ0lw(4&<$@=*+vA<7g>N@IHhoXZg@|Lk zIdHh$bNBL&l7)V^Lt@~NCK83s)fL@#M^>R5w#3~P>I32<(phx`yhA#V#E1(K^}aqP zndz^Jr2trNyWq4S;<3EsvMS|QRI9LE0}f82XtU9+CmjG+%LtmYP$|D^X?qrdYwJku zk8@S5u9%Z4CYl#Q#8p*w6L6I3kQOKfC(d*yFh>>VkotMQ=u=xKmU-H_6kPB&hJz!e zf*-6J#{T7J_>`H($s)+!85<>Mi|SX6islG85esHC$Gt)PoDlm3W^ zH_%N$e6g}3ON_DW`6eROP*a_S&6(<=wqbhjA$V_ zMS_Qv=nLHa?1n%3-G9xGfAmlEF_6Xs-Z^}|B=5xjwrP!Ow(F?YU+|_|;lv{TsR-f) z=L0cX`iiSmdl6d$wB(sp(dwETpirNSE`Sy%gy8YPA+D;+pE>_ePFUEUPhEt96f5W5#Iu{BQLyLH&T$F!Q`F2uEd~ULm5x-gnWx^919WO) zfV;a9oe~8{DsC=I`c_R0^j?})^YTb5e_nlyv#)b&cjI1j=1o+;N-a3J`tB3M?wjgt zKb{hxEB~gMk770Ct8MxE=f4+Bfu|9wN6!DIzR}mwUf}^`tt4jM)u+WX}T>Z zC*CbOfWptXwLuYY0eJH!*wPV6X;vEWuM9u z15mV1(sqo&kz_F>)0Xzd1eKN-WqnR!)VwAHB+~9*u9wmz0o&6lB` zdZ{VYvV*VLbP2tD&i2UH3Fnh;<#s-OE`5B`o;!+{7S1oi37@~~DF<7a4Xe-lz4!8> z&LUmQ)*>m3R~|n%;Li@x>X0^QuDW-NPb`jo-2Sl0&)eMYIvyW;t_?VxSCz&0Ir%C; zfX4^HYChMdi~rR)21Tq!_%Lj^dUl1B12K4FAMm?x2?qZF03ZNKL_t(7&p!S(SKs=O z!<0chUD)89{CVDf?B0-B&u=BT4qU58eBFUCmMS@hFteXk?1Ih*m? z6#Si>M5D5tkL$Ju+B)Xd9L=1vG3BR3o~(|ur*+&B zF4=0eYeI4%9gkL<-U*_P{oM^WpMHkceCG^g=ZL;4R(rvVqEliB9ZX8O8%cK)fyjFw zeTWO5u_PuDwpZ75eNQSLx4pt`wqzeE&f&WroHr4-7$ACxk>XA9U!ALtyHWgGx*1}0 zb

5htWnR`1uC*}Ok5oq-m0ZDYd9phW{*a5(X{7b}QZo_kiU5xbMS53 zr-h`2fHP(K&QcpxsO%VfuTqilm1(C{HGUZpUovcRDM-n+UVVn(NjaIgW3*C=VM=5_ zK^bhUIbl2;t1xFV=dsQi#8Gmoz(7HyP@Gaip-8~15Y$*Wez@kSo{d(AMGERlt-@%G z9H&sE%z*0>c$p)Z)d6uB>!#IF*OE!<$V77JdH>gbi_OQs#?j;_$+x#`edOji(#4Hc zlqdzMQFO)$v_qW3%i_6Kr&yd>Xj3ZO?T>8wt~xBOdrAp{3jse)Oh0+a|MoxrF8}4v z{|Z&*)3m4W0=}5DY|fBHYz-i?b z2Bs=VEemRuM(0w0Av$7o7R)K8_h_4uBHpxMTlI+E*oc~FkYQIZluK?;;|zmmZm#iyF! zvnjsbUzN3eaxY~?^<-r0=`&i#_d_<(%;snk0tK9_AY;Kcb|*}D)R_~#^}=;@j8kGP zTGMrP)^WLHKU@wftULso4u({4TTTXk^>ZHH|DUjKacK2-m{-TL7#l5K`Iyc3O>6h< z?F9g^07Pt>%Do8E{pZAk?;FOv_LubD0R0>n@eOI0{DwCL0Uk{Or~&MU-v@hW+9rGb z{n!{>-Sou+Y|9w{34dAHU@O{j#-R_}lrlZvgt}dLuPLBU*^g;id52 zvuie8z~w*}1H*PehAkg|=Q}v(8K<#E)EpA3uvm+!>YiqXScjc_;tM2$Y% z4BY0}Emhjjh4I?wjasj>fxM*NF7suZkqnNK16f7`19C>2LG2tKfu#f2BBJ8(K7zMg zQRf|fjNq#7N~Le(kAUR>w;b`MP?_^4glFpir?P>D#a8xNb(m2l3AeKAx())L7b`2H^(8yNEfXSFJIp9>eUUXu-)w#HXA;> zeFfg(LPSC&^c!60kzqJhUvO@w`TG#@F&gbW1e}is0Env&WnNl%!~fo_x_|B^Sh1`aU}M#)Ow0zAobtfO zqon|(l<*;dTDn1qk^S*lQyaii_H4ddDhQx0(kpYo!#UwN9+BXT6>#tVQYqVR9(xZ( z_?qiK10PBw1V9N$*OOhx_Wcj}-Vc7rt0^&wfkPTR$8lmz6EQ@jRu-9Zs!ma18b{)= zneY8_?g9q7IJ>vUX~KEIdFr|YIDd{__@63IF74% zVkrj^@A9lq(C*EWZE}81t2xBk_VYFDN2c9k>-ph0avUde7D^Ulb1lRRzY6E_6f8Rh zN0#rwY416$8H>3ii*u+cY6b7;x{l38>65ZQCUdU8@SFYk1H81JKKeJ<{{N-)^>X{B zG6}wrc5cxA!W+0xx35+4y+zA6b<&({obOK-3>E%-nR9!;Jk!c*Ui(|L5P`e9J8o`nP*t8?U(@#+rZiE2?RJX>hwlc$utj5! z`pzmTIERFQj~!Bl{#*#)4JdIw*m2@2ujGD?@3P$2^0_bD${}5hk>>ob)Vf(m0ngKr zY;0VPWVGYY^V?VV-<@i*Un-M#K2~$iGq5$MDR@7l{@#1C^Cmvm!)~|Z)i|M;v*^4> zi;bs#7+98Xb53TP!`I3dT59>@#e68L9Mgmhoy||uS$~_2ql^u?Sk2$FdYC zStvyS&p|x${tLeMFMpfT4@_gSGLg<%MF3HEg377n<$^tN0%Mrj z0mXrONTP&b5}@1SNuS;D@BZk2=g5D_cAne%EKEbnD={@e45F%m)(lVq+a0?goHyBB@m(|0|`!(o2? zcs$NP#Q9vf=ln0`&)3$((xHkfQZN8u8c8LR3Ph2b9<*M=zr60803N${_oJKZHNmO| zobKrw0B~aBxz0OsfIbHL7zk6I(+<9B2k@zTxO)F9w%_0O8Nc&4e*W4Q4gkDiJlp=2 zQzU=DLn%1ZGwod<}=M>BCYDn)4aIjeW^}^?#Xs1og zZd_Xw+c}n#`0*T($0z*H+k}3|i2Se@1*`>uWdW^?x82;AGZ$D=ADf|g7W zh!j40{+v#f=!NU69f)Jf%B~w2Hd_v_Zp?ob!G(zH%yIU^fa`ifE$f#Qu zym#jiZ$f{DoR}hiDW?)#(=^SI%vS$>9LM?nB35OsUCuc(9`Crly#wI-`kJomn5K~|=A0Gp z>9;%j?G+_@viMrrz@m6Q1PB2aVii3MR!7~3s{C(seEXp;K&Sm;sieO&w%hZ*Pv1NV zvbE1I9aS4c_bJ57aU$-lG}vPNEx%`3MPi+9Q08dvQlxG9*>mt<*-FzJD3&_VfSASr z2qDbufRrY1j&eM*ySl1TU~i6w50uJ&Y5ET>0tpUMMif%5457WvPo0hy?9ev%^j^Oo zY9bC?wSUB%_Cr?i9fgjv+3?-p{4KiYFL-$z$zn{P!*P#lVs~{#o=iORIE{5qRL9j> zxyY1G6x{}(G$ugPH7Y_Cjk7YEDNS_FF}}Ltul~)S^2h)4@8PD*blfv^(SS!$2%e?3 zL3@57m~KaiVSaxZMW5&I0wh_olxDV&K-P2*Kv_n#8vrRaopdhY=L{e%rOd*BZJjW- zki$F2{_d0lvs^F7su^+#{JL1yPTxAY@?KL;^XSq(R@6x9s)ekDRFvEOk<5-%!Pe#H zVQpKS|D50F$L8Hbt<7KOoRxq*>+|}0nCH!M?il!3fg=z&!8Y>F@QPdj1SrgnxPd^>uhT`~f{$oACtJ$3x~ zdtY?_9*SlyzjOV(o|pq?8|*{xziHpTn)Z0#fBxJ0y3U>8&(q%I^Eqew5QsYQ-fmzU zpmT+x3zVGLY_GUGPFz2KZWTm;=g-MhJiakN_NU=dg`+P(yf zW*0T8YUsMk4DfT*&IOCgHo>k&(OpKG@;IWUFpd-I3qcyyKbr%f8CPsqnx+XK!%Xp( z=7iKpq-afB5J!w1pZ@H}96q~Y+8<05Q5EqPJwz2RYf$9ntCt9#VHm7jT{7l?#EyP5 z;9`$-0~)(pTit>8NC;Np!AJ1XYRrd7@$;r?Y;<>0)UmZ+34U#~H;4Yx^StChwMgpn zI+hX6C8OXRTw>nG*Pm_YZ;oDjf10Kfdt-6jIZ(0e|C0I9j$1NWv?y95$FY|6YXFdQ zMjhTC50=`3(hb9`L1ZN}^GQ&#rma{Gx(Smhx?q(EMGIZuF-^gq)718;QzY6gbcr5Y z(^Y-7pX~mpQb^8G!a(kJeCr24Wb@ItxlJHpARQAb;9aQIip;?Z(Mm}+R*qtoDa6Nl zZD?boIj&nA)n0@&CQ8cu&7b}`fAR-^2;;M*VLNy$_yY=;-=LjJ}o?v~{Qp z28gxtpa%Mc73kTkoG2lAUv*f9 z>)w-rgfmd4ovDHE=j+TvVCuO^TTZE00LjTlA|jv@((Qop#A-(i~DNvm=};Qq0{f$yY{Yq?M>B$PosBy`@`qx z;fAa0Eg^U)1y$j0zvsh`zQxVWEzU=LlsO{Y^*yokNEh*aPv|;SOm$quS{awT?bo1%!m2oH%k7J3AfMDl^2CtmL3^n5}{I3mSd?)FSn70sD$*VFY@e(?78j+`>XU{x1Xm1!DLVAuruVFSH^0L2H?*Aj@q zqWcmoVk`A;s%pJqB9$p1o>_ud1gg^iLm=b{Xg-a{^YMAk{#bq9P6Dh;)6L%{vf}AA z2ea$C6_9Xgj4j8{5(GHcq__msuEwsw@i=lk9M}xQ%s_E+Dp`4)Mgt#$w|yI#H7?Q_ zJ-tO9MOtpPB4Rb}6)Tr{myYBh^t%o2@Djv8ELIgu?gKEZ?`Z}_spte*HJNRVDNrZf=k4`YUetw?rRM754iB-i0|8A_xXP$SIwpJ(FcW+TXzktJ=`+ zGXjT+VN(9%U;Gt+@_WC}{*zCM-jSVByv<{rR6##h=WfpX3_PB#FAV^gkM|zG0EyZ& zZy=)xP{Ir*H3z%_0kczCL59BX=V}3}R%XmuKlsOAi~-2rc+{L&Why- zL)WqGd-f@llaeod&!y{m>bjQiE$#Cs0f5J^nC9e54RU^!?H4z&^RBh_jRpW<4FXUn z_YwfmszVNjXtxymZ^F=BmQQQWx-3Zn${Tg`d@=s2ghAf5ckNyK>YMi-C-BjW=LD3l z4|r$gva$+4{prte!+`Ui&tARaD8g|}bYX+}CUO$L4dQ!+8XR$_L$+g1?3BR*CJfXDl2 zDFogC0#iaiMRNPTpIHDc3fO)GbJV;DT22vd?DkkxHUvt@TOK5!F%q; zk$xEX^dJ6_d>9D`UTSozI=rfdp&RI82Pm&zy<(aY!>~o9fedzkyRM@jdP3~b&Px0h z@6F-wA|ZOq?+?);x%IyICS+FAArNCs2qpq(6+C37w=bVrYv+-@d(jlu(YW*0B4p=I z`8<|HRuQlU&Mcjc<(OFl2&YKqDF=8N;a`r)bA5p2d9#-4@EOBH#DQ8BgxGcHFfdLN zspJ`y@-dQ9q8kP)BPdoXPg6@BIprWX31C;9^eJbK#{+S2Y<4^36VnOz&KpqROx0fX za^CfF?rCvY8la8E;)E0f{d+I?;0Hfo>N-e;DJf}EItQHuraa;>21-%2Qigyy$5bQK z-g~sjYFsRt9`ilb>YRHk;pvO=qyPN3{KX&r3AaD_#L59q2?-8@SRsg7g3ryY2RA#= z?cB~edI@)q z=G=u^L1EdKRu*tM?ixt&bw^KCme#2QQO0THFpem(3iQ8TgdZ+PkI&y#`8Zi0QhpgX$ z0KeGnjRAnwfOt}7zfp^7$*@)QURTp!FwvDF82t(6=)j5mA}1h#W;+RsJ00`I zt}t)9fG*op$A1xheT*XXHYIrK*<^*%vN5v--r7vrAxACUUW*sv2$P(fhB$GP+q$ z!aUku_x^s)+oas9^0I1gH#eVdB6BwM<@e`%`}sG))$=@ydKi_M1uYvK84q|-dQE(= z+48~jD-JiG@ZJaS1Il4a><@ePIrH(yA2OxM9DQ+wq~vjA><6pAz1gxEo-yeIEu##}jj5C)P z;&U~BJ{?BV;S@c~Ig?Yu`#^A>uJ7<&heQ*r0&1lLPtro)MW!jy4Lv278Bh=b@t)|N z(WS?clI+}NN~WCeJ>zjCde2g1s-Ps4teKLnycoGqN(QF{=a|NkG#-gb`Po1IBRP$v zaU{*9$b>XaCLq@L2ylD5=Xe|$h7G|7#xaxfMA!8YBJS!LY_{YOaiOC~E8}Mll8eDg z#s&Kk=kU&)PwRh+`h%4?w8+2JQg`4Z%q)ybMQ(|BVj2`D!@fCRimKpTK(I*WoX$7v z-3z4vauShSId6+Sd)byc0(M-h)80hC+LCG4=iJG$YEIo#k;*pSV+csj;5?gQ05xI9 zB#^W)bR8*m^l8KR@+Ijw*$V6YJeO7NKILL#!B=p_QV|6w!G!V@ie|(?zZobcp}uD- zjxB*uSuQ0AMZlL@KI1x z;;b@)iXc9cQ|5SguyH5>F9C`ms(3Pe>>2vbqQ!N9IdAi<^A*Ibx&K;0s@Qd;oGk@G zjIKVXyEspgY`{?PObSjECuL5Hc;W)h=8}{B)od_1d&NM{<`JC&gv;w% zUYjbWIYNP~^Voch4Db;6a(>^ee`i6;3d+vsm_dtEf6mO+#sBY*v$wa;b?qOWZ5M(( zY9l_z?ywV_vrx`Xrf0VCH?X}eK=Vcb!2Q7`k1j%54Ugu;wuNTS^Vec!sFr7J5q!0} z&+9t%y7IZDmW-T=oNK6w8Fk_xxCcZl=c9x}J( zz_&Wj%bOcig-Hb@;D#;3Zi62-#Qusd^hDo5u@yPEwpklUx}^E8fPm&as z%P*$bxO36t`i@d8B9|zPV`8`4aeH^i^XJbw9*=Z=N9;QeheM6<)_E5La;WvwvpGOH zCDQ=Ng@UwsVM~*k2K^i%Zqg5%us=f$z zy&}Th-GRg5P#s&xIAyxNr>HrZVRwz}uF*=w#;vC5#?!`7{_tW4{yiX{;YM*DRGfIF5b>FmRtoGJ<$j9 zlyIG(*)$`3H1MRYhpFW1F9|s*B`HxttxRFgv#Jt8hdPhDdd?>j@C1Bik|a?Ohp+up zj16MO=xIGy1<{O1q4>xs1F5^_#rM9?^#|YOc1rkehtE*TL{cSFD7jQ%rrry31%nLK za}w<9idYFrX-<$mTdj-%^`22P(hL5mgyY1I|LcF_&;Iz2`G^1fpNP?MOb0q2>R@ul zQ1K4&Hg=p;Esfb`D>zKl(mA2>wsFy_SrjLPZjs|(Z-?`~I!3Y@2R2R|&UdI3N={Ie zGASe&LLgHJP7$^8pWb(ftGj9`RuRG3`e-@_&Z=vOFlk1jH;1#18Qa5+2@<>kqSGXc zF|9KY;GCG3qHRwuH1BS{KE1#HuJ@RS#WPpOT?hn7DFiaP+SO8@_1*EvR7@mL)zd)4 z7C?CjjPIjeQeYO8AlewOy!TK~g@ zH9^$Y;fgcVBbyJkzBeoTraH_IGPJ1HpPFys7Her63o1Gf!M1mvx5t7^Z)dNcIq38C zx3{Zp@<4Mu4FZU)?Y~ujg>N(n@I|mQzVsA;wt3jo)5Y^_qx1Aud%$lpWEN*X0Ynd- z;0!_fGTMC#(E7FYF>6Ha!pZLScRitWWq^F$l(Kj2>uzU}uL}{jH)~(=Wcej;a|9k{ zLn4R}RM~Wa58vfO7f^|~-d6|S6GEIFpO#m? zthar>ao_*P zag^F;D-{+^DH>)6^V3g1<;9B^+}_@@-ELQ}91cfg8B`}{N{N(5 zR09H(%0g)8(;D#=6j8|IktmM+apY$|{V92x$kUN=8t3m5F-pIw`%m2M_ozVEb>=KP zj|HCi1YDe**f({mqb-p_JwG5g#O92S6BgAg?x;PKwS8Kw`L8_qmik2&9j51y3 zRLkmqUfQiM?RCpEfz|$aVR>1CjO};3e~o^9=`-!QE=2-c<$#ugA|i7x_B}A8cBajj zmL3vAU@UvOzUS5cz%)*Dozc=`jPtl(QrDYcVL3BDopV+iP?ZoOVHn6@AeXobxYhx$ z&CEZ|HyWsdnDc|w2x??1aP|HN{K{|sfD|J!^c;>S}>f)9*`Bk4FIz%UF<+1N*yO9Xy3_F5a~mqqQC0JV*$<6&Yv zPLxf*N>#APzs}kamz=$ogM99go;6!S9-zm!*B?2LHkfj;#+>H9n9!}9KoakS;!LQs z?;@Knb6Xi;bEJRv-=B6y&TadNM=a?803ZNKL_t(19kMeA`n3DE`|nI;&I9ACX+yt~7D;keJMd)l0rU;;7rcpt!f zLf10^VHg;$u23IHQgd0WV~;n*e_Yi%s2Tv`LUsBpoplCFPL6kL?Kv%<=@I~H`-zQ9 zSvdhKuz>qU@7j20e(uXW=rw?Fe=IG3FLU_YbBDDUTN{7v_4c{;S_5C&acvB?V_Q!5 zvZP@f!zE?9*l`?_2?CBsdf8SdKFsI(|Ji%@Hp!9Wy!RJ*0jj!ZW-nZllIe?{qvP-Y zB`jL9sf%UOvSi8fk!4dP_c}A(RRv^boIfHHl|WT@&+P0jm(m2E=4@2~2_TV|h&LkM zX!qOzijbxYvLls=dCtq+@B4q7D$&pX<=^^_n;w!WsJDoSECrb|F$L~F`+`6F4?pE^ z{>xty6$aSOi4#oEOo>XqOrR;K8a{NXO*gDr|K3-T=(=J7xt=wR>Nx8RSf< zDJ%_GO!Hj%M(WraEJDb)6a#Om2jV?L%ahqDGfw*bI< zc`x6!eDeUnW3#4)`~ZFA{#xpyWfSGiCWq-7*QU}M-Ttmc9zYP+9*~DStSM2wY`ru6 z0hdbJ!wOcr@;e3STnkD2j-n7A0zAHNQ4o)XM?EQ<@pGYUZ@#;1|Ga5lzxLzzqpDXA zIX}}A5c&5+wqCBUbN*IIu@C2Cdhh95wQVte2;ik~GX`#?@bUAJ51#G$$&Wwg*PnjL zbUN|HS1-v7>~DAMo;^pS^5KUcgVucOB6uHhE-=Iq=fb&Wc4sKH|L#ltZS(&v1;Em? zR-j~eely6LLfac^s)|-d^=5Q8AfhS5TC4{7Je{mvEw#-|DJ@jiLgt-<^Nw5!hr%+gK!A3S+i0l}0=wM~E#|S55SUIU^XwwHg5~k5 z0lK9WrsI+Ec1IioI!(BgNOyM}U%o<0GS5?|bho;`+sO2_XhuVJQ~BhKj&#zZ*1cZXvg>E+3J4~ zVqg+sx7%@lJXl2lCpCS;-fc5F+ck%3*gKqg!K-)Qi6=x0TaLRuyXP;E(_?LPEWSkzTcLn?hX0ggML+)?7+*m z4)o7+?Kv;^YUAN@wKap;AO^=i26k~~&Q{qa7p0}UEnvXckn{T4(o)vCeEZ!A$O9ql zy3y_5R93p)*HCxXYrthx@ydSC%W?QN}I({~nSl!F_t&g9Q zcQyXox!2pd9jNS z-2R5I?oZ@tLU2q|!g*u-*J2*T5C*&ttNU(-PBRAjGG9&lD1oBIl)(11ozdT(bzxKA zFkL(6C~{UFMP%`Qo!R(vO$k_65U^0-ym;bUXSqfURzU?$Cq{oR4c4yRJi4c9#5aSX z2RY1$bKpJMrIoShuo7OU!+|7cBWW0hwkeUO8BlVX(0M|piC3R}&hgb(NGTjoM@q>I z!?22~XoliAj)WMEWKC*V|1c8vdt|H?8(gfV9S0;1X7I-WLNH2e=P8I)KL`Qw!O{pS z<3G|A&`TwInaAC5?Y(EJx-i7|>qf6+8)&nx55L-fovg0a8=yA6EC;`hn;vA^j>&!u zH-omX2(W#&eja-77dG}5wjHWxx0M`hzi;PD^Z`x9JlWOQODW;UVbxxn1ML~Tx0?6% zOwe{)=g)nTG~8NqI?waUOJ`fvu|D!461*|+hZvT5+ei7Nl+GdOvi5Md_igDG+x*_L ztY1TjgkiwPfRDADrZb)a1Y!vIXaJj5&>-b=P3K`Z8aP&qT3*Y4T z-?nbKUjKd<+*|J^?^6KYQF?{w5=nnIFr~HC|MzOaQ+Li?;yrryUf#>^k}O_QRiY18 zFMGP;ABC^Je9b@o`W5$+8HdB|4KnQbYMS`!a7T=ah$jRgXQ>LV65{{}F$|06 ztoMR7L#26Qm$F-(r_I(9M^A#&uRVPvKyUapBisx6$OLpjEL=@6I1LoNht1U*1 zk#zECo|sZ%h>@HUa|;)Dj^G?AP3*m6IvjZY`RC{~GlszFJ~561!!U9>otA!Uxx>RS zRDvm2bNd5+H{!;T>>MgqdeFrI9|v5tst3O2`AaQ%DBgGh%=6!j{|W%G&h5YE$=^l* ztoAVk=X9kYUXwPy`t!H@R&RvnoGl%qdWYLF?VYtY+jzJJz_oF=y?0<~zX2|N$bX-L z(xi>czOP#QSrd(5tR0(*l>?ln$;L??6AM2?r@)_`9n=tVP<5yli?@!EQ_2)PUO=28 z>Z|Y2V>DJ`d;Mb2SzjI$!pA@0M}PS5@gIJ~%cK;n;($+?dA8JoZR$*Wj{bf1^LLrA zSQUtz3*tOG@0spT$T9OzfA_!m`Op58-~93)Df7hO1Kv4&bojv|(c`f4oUY})3k)?S z05Kv@*>IA0XTVkeZu|Gu-m9wOg8>CSAl1f9&Y3jN3#~#AM7A;IgYk$Uf;dOqjT}x# z1SSYB2Gxtgz7n(grI0HjN@ok6J!m3wsEpvGv8fDd`Of_19=7d z=E|obRAbdqN(KoW4~c2YHDh%>AN%*!uC=z1U#oa5!dw%%QbI$pg1@WPV&YP#95uR-ord|5-0__DBk+c7iIo6>(c94P5z_fK8xu{3~j9P!Ta`tBZ4 z_~KW;=Ji)!A@hmo9n*B;`STYXkH^JR+(Q2s1r-qQh~ti7zek1cHQ(QR11((h_#2Vn;{9*I6C04af7e?bx4k=i|7#EaR+H^L-YiZ&CB1S06gb=^WHP1iF7(~ zcX!7}AAPh6@xB1aa>1)JC``?mmtynPJIC(kme-1LEwp|>YP3uB{#OnF?>&O6sVbgl zFFxk>5C0JN{5hZ9AIW~iJ0ai@BfKXGtH^}DuiG;{1OT)sOaXG5$#W*8!vFY-zvSnC z^q)8#UZa&%O;w3Qz(>K45g7wsJzg!GKgMXFRt&~0Zkw2~fV%{gCyFb>7jf1{{J_yjkqooAhCWh{tUR)n)?wh>MqYA+9_~u)Xx*F7sLeWN}9N=dkuM&}0aa!`(en98#LMObo0Ln5GkR8qqRw6CBg$FFAhs6=N2> zA307ZaFOGbIUOf<6k@WE}2gC-&tGHW%<>v_%KZyT9wr7LSVKNfzz z7?Do!&R?{<+()r8xYm@b>rDd^{7TvHmHze_&H#LGfb@Cp-h0MzT!r{wGfLX{ZQxOi zkpRg+Hor6{QZXZ3wbq=+;gV41%yfUGR3r0rJeuJs=ZXbJAXoF+MJoHe?cE~PfW3Xj zy$?j^aU~fUra<YgN4KFzy`9FMru#OvRD&VT*m|I9!B_y2*P5>6AjBxCg-Y8pNNVe5183}B1%j3Dl zX`Za~U&@qRDo|>GL>vZ0yroZx0WBep)<@VnIMwK&QiK>soReBIv{)4qKyx8aGk8x7 z##Mi>my=C97o5X% zbq})>?uv3(GB5D94@gEe(MrQMu90OfUj+!)57ZO&m10-+u9X+8`xMn|&`w+J*RT3n zmrwPt*T1VGrvmCct-PXvxL7KWgWw9$C3d^OFlA1uB3`TKdHAI~{kpURPU_y(O6pRB zAF546wS}~MGOhsj-=s9{)0W+4OKqM?nvT$?GS%~S3y?g{fn?9OZhswt_H+tNFOGFp zi;HLwReYa9y@~0+W?%eElW#r(pblg+^h*T*?8T}RO2$IVu(C!XvKTpL^iy+3A0a7f zN(~UIV$vdZoLab#Hu$Qh}KEnK6sc4$?6 zIM?Z1&r1X=#C`}~AlQ{2zz%R74*9_r_Fc%CpP%T5-9s5j;o1>HM}2)UfYx!VfmLSPsN$~-aaO!303R(j0i@d}ca2&B`A zW11OuBfI?`2{pXAddVdVXboenHM&)e)jXGCdrUQIr)g$BO_Y*}VIa+lrb3}GYa)&T zTU`@*I>2<~^{2lfzrJTzOYVJjyrU4fyFW6909fexIF7_>2nFxSaUk5>AmfNuX8(f2 zOEAN$tus!VQ{Ud9voKbvdDS!{SgK(sHCGu&(et|2yq>L-+uD0=Y%fZ>G|8Ms%jx=4Vnxy_%kr zGSbF_n2OzimPJ!>Qq_jDrxmr(|I^__d41rQfAe?z*FXLfzWmj%@T#Z?-m8_*^9~#q z(jSe%--!q3@X^Znh2~Z7-+wml6CVPIkV>|E_|gC-1DPBwZ2YJa$AOeHhv~!+cDNYH z(@aS-O$E|M;Hl>_gRYw^E(V5ite}m7YCiadps;}wqDCSrIvK%V&Q{GJPiS2e2j`7@ zrPw;6&Y{uaTqG2$22r!h2{pQ>%y}_)mi=>WpFAo}KWyjrb7xtHYwJA^h=@gz*tHym z^E#~<$aO*~BD|buJ|>WzqLQJ2Q$g_*Z6Jw(c>T58+`1MCu!=AdL7mwAa)OllzF?{0 zu3|uC0}NGI03zxdVkEkHUPCB#9dr~Ga30MGq(xuVGL}wpKC>Gf!|0KmDz|}HRN7;G zzpR&+NA+vi3P>nY*G*TQl8Huid)7T=SxXlSy#cVwa@02=%^qr?Lo3C;JSWYK*?aoe z8nblvLwhHWU2g?gq2vpl>dl|8MN9S4^oV5)_LiBh^)ttN5a2sX0q90n-PE64=!7Q^ zx;7uqUWzub#7NY&;roV%}n#RoSZahzv9{o)nojBvcXKX5#KMaqhdku;sX z9xix79GFfC#6sieoDt_4#@#{!*ei%|Rv4YQs{p{+K&#JC4Ws5oUD_#Xxvm?pepCLW z3+G({n-Bc8bkG+1A40HvTKcF06YqV^({<#0pybRvSv~HhESL|35FtRRx!R=?3p(e} zTu3P|&mUvttCz0`;uyyrCwFAd8P#mj9C-$(IH#m}W$A4hiGNcYNYexX!M9^>yPq{9s}ED|L+RUI+8g)% zJ@=_C+j~_*4lcMMRCPrP+4oIz<@2I4p@ zUVaPfk9Z$D9tT^~?B5z;f*I(gWK=BjLY#%rJLkxCeoF1b7NHSD2x@bF2(f~c)krMG zO8s?UWdn9vD#%g=BGhu4-k}0>jRYCSffy`>ssTvbDiR84NkkW!Iia<0d<@7CDapZ- z=48AgrLJco1iUJ#Mp9Y-yafXAhSHoz?cdF>7st1;pK|7KIFiz6U~o1hst51T>nRuy zNpE~DU?kPZUohA+I8e3e^ZBwNeU?k$a(j*Sn7_P+bDrJk*qxMk%mzjSwCIL6f4qmD zn!b}(Ywx2+e)n?i`j18+JQkk%M)Gc!S=&FB*6MrhLEQK9xcqK&05mY+?9>42=iidu zyUKxlXyUx#`EVT&Xkw%-rCZdrTjbsY0Pp3!e4nIw1;7-@U37eOyN8k(#vK|WpMLr! zU%oyvJpYh?{LQDR_ncCqs32O%Es#!?xfHURryCrpC{C%ye>4(3cexf_TT7ae)C_jp zM9;sq?^^h6DaFdT>G|1W_4X~KSe9&i_1gV9XMb+hH=6ST0<;u^R!*?@G@;56B6CiZ zlsO&_Xer#>-ZI1iFK(&L-SWZbc}BF|GfaR}^hZzXPErUwaEOHRJc%p5{a@ zbjS-3s=5766Z%t9$CnMB8}jfv;=N)c)N90QO!A-ot*d{WzU6#bJ>HW?{bd zGd}k0eIFn#8t3~d*Y=3b5x-r)LiOVH^c*7ZwSXybBD@qVNz1O@6c(Fa^ zUHjm7OSXW__PxE`=CM_x-}Lxi){FM#J-&^oe*GDwEez+MnXKp1)5n{hQ|EN`2s-C^ z@!~l<3iXvT7oxgcsHo1o~(Ia;>r4kz1{*2{^IzD?Nw!uS2*^ z`E^y{w=mB}OmH^LdJcgGLiFYSoc9)%t;*?GN&B2*825-b&mFZSSB#OTp!q&Xo&;Fu;(d1lU;jFQsK5FEG9 zUVslwIkCTeP8df^PZ&lLf)is)ml$xvfRDxpfb({ov&aBTn0HXqK&S@CvS&OoTdx`Z zTW^0KuG^0hIFF@e9#oRMt@pfdzx{Smf|RpT-VCiaPJ1OT=QT0qH4tl6LUME6)?Poh z`|EGl+I5M%S{)#lT2ih?5;QM!1;ujC_{t7HrMViq-Xb;JI`XC6-#pJ`SJ2>*IFgE1 z!nGE)Xu0NIU`j%cBeM+bKly!bKKXs-VTX%5bXu47Y=6yDzJ0d#d`7|ubGA}{l)`*C z;7jI<&pzY7{Mn!JPk;UQ=$H5SQgA8NGKIphA1OhQ=!rGtU%aom?^gcPdt=yloFgz+ zYf+o$&gQC*0WX3JzJ`7KCFCEBAkjI1qSJ$i_bXf>M zLs-(Gq!RgwbKKnQZ3nx|001BWNkl~@frJ2E56hWS6oDaBQ zsad{O`y05;u4_LJ&wVvftA85K^+kY~ z+2}b=GjmbI1$)-Fl+}Ga@ckP*1@+r?;28+UZ z^m1V^TLRU(-7D=}yM8HMEm!FmIg18xHOH&9bD`K>La=W&NvOH~=ew4zaqy(nM?T;Gw|H~kv234Td3pmiJE@X?QMlQSjH7wV zr$g67K*Y$w5^Lj7 zVez=Bw+%192cp0P&1|pm=XA|D%=p6CaQ`7$KmqQ2{aHd_bM$jJHyNUa+wLH=JZoDd*e& z-j=sayc(sxONXdwD9eoNxW}w z%hukW$9qq9j%k|C_wNjh^UgC(Cr+mmw=v?xRj*+)<*SNUf;X*J# zM?ivyk`0s-N8mgzfQ3+UnkI@X#xYXYx+x|0yAcV#){|!;lU$QA(`m)^lQwJSQs3C> zh0A!qf*16^Z}n0O+wTu%F_0Uu+Z)fPQ|5Fklq@uISXb1ljJ7dW*X*rP-nT>d`+AIC zdT<|;?LKv}A8!DLmQt~N2h}OqHa>?qCAiGsl$*gbIOR}@Pm3!43H@&?tKUVcNwe1F zuIb`e&p$7a)!u-uzW%oIb&TML@AWPFfgHbn`=K=-l!~Jw;*oAV*B=g+lJtK`Q4D!#`_-d{H;nu*oR>t z29HqCoVdTgC#RWD_9H*~_&JWuKmGbQeE#x|V-aRNT3`lLgBf{h6$Azs7{no^kmp3o zN>=dB6KFqqdrj3W{wcq^Bd#Stn90Aj#Ei01tlhlnxBJIUwpudOkF zTz$V=MfqCEu71p1Q?wfgKp%C`?_vFUTkrcrdsz=K^-5gxzBYqgM6QPWw)<*rv}fLq z+wI=EJ-2;co?BQ*1#>A2p=OJ6IUEk$+}@s#tLKH_N$DJQ(}N1$d*%SsRPb?N%1SC) zX}OIE5@t>mk`K%{Zh!D&Za(}W$?qsCmYPtwn6o|jam}gF3}Wvbr`LDvyyuHgKjSa| z_)qxd|NMKVm#^4W(9sVOiJl;WIPhbDeL#aSgh&vN3$;9_YlM}p+JHly8Py_IUJYld zKDnI1O!F!m;iR?Ysv%#&px_s-lGeW#=AVt}UtvB?#wcH-HN;-qlJnojRE%+n0Kgfz z=c%;J-q)~m%YiRBfeS0+xUD_L0bpc#E&PAn?}$Sr3Zt42?kA)_#=zchM?u-n<92OvvH+&b zp9h~aL6pJ4K6>tENCjJKzhz6}`{tGYzTMB)kuA`)<+gb*XZhU*01SFua{je0#uYTX z29SQU*RK#=L;bHUgx3_6t!x#~zMY4Z^Y`*zwsJXccn<>n7NwEk<($cB2F(mLZ};x? z9sl+xAF~UN&p-Q=uU@_+tB}=^3KS>!5RszH#}hGlW0Wt2a++|!;cn*saAXJ;Qf{Pq zdwd9I-%@A9U92BhjDucObqV3`LueZqP{3I9I|Fi>CWc`!59p@7KKy^4XEbd?y3d~G zl#FE0bwI&F?pHmmu9CpyIgvv`oGI?(IPm)Jj^Z-Y@n}&6s*Fw$3E(VW-+4z20jg)Q zv3eg42hubVh8?@x8+La)rsIJqV&1$oS<1lizz6%0`807j968MsMGHfW?Du>2yB#^r zxG)fJZqP779Fbv<9|qiDr5eL95QYIijHnA}^{R?9k8+3ucyHb-=d7PxtEJP7e=~Vj zZ&+BR2rTck>(ebkpQ zqz450ao7_G_UD3)%_R*%wR-j~XWq(}RbazA;y7@cCQBWeXOu$D1#yn(!>V6aeJ4UL zX2kE~z??IwfXj*tg7Xe>k&|Rf49M<=4?g)ZVZ3E74(~igty;wv0N4T?OU{1>&;jV2 z(0S&+{pHX3`G5WyFMs(f{AprLg;10n1G5yOcXfU7#C^o=3;>8B5?weO{LYLl>iZ9I zz)4v8Jaq4Hp65Eg&9HPe-AKHZv{Y?n>@4!9SL#tqu^=m&3%-{1n@=Z7HLeA_mg_*M z20*oO;}=8O=2FqZoMyr>FowW%cfX_?Ip-LLVTnGnN(m6W$BV~}KnRw8T8-_Zg}Cq? z)UuB&$63(Eyr`Ivpyz$DZp!> z<-0z2yRZ7qW`Eg4ML5B$@h=1?j6t~B?-G%swP-`ovu#QM8D*njdJr%=9$eer#F zL~cRuhd}A~VzL4Nt}20#o-T-XA%ET9^1C>iRo;q9smKz(zto4z7|GQuUwC8 zNe}0+vbU-$^MHJt6K31D7YiWkGI0v4GJDrbcx+MT$9|Wu@obk1b!k7eJM0eL=FmSr z+E(8CCysgQHxb#I0~gYwW%@5#nOYH+N)m4?eF);a%420(thYB_g^e=2{rpXeBII?% zAFS>tqh(%{ClSYVIuS|y=!0i`{Ne?NdFIR4C+=q@Ng#`n`8Drt@Ph0{VkNSh^Gr#} z!fdC^;pKtx2YbR`<(lH{fChVkP8?dyL#*1$mA3RHS-snbHp`;taOu{EiydzVFvL-ZPpDP797=g%e9ga)Fdcb2^ipxk^4LR!*yVVv(9&pvt)4 zF-Bb^WlI{^S^T@ zr2}Iqh%21t6Q|>ZChOI~2lhiPj}~sp7~$Ix1_%)!El=1-V^4R{f3>A=#mBB+D%~ zAN@Z2PyQ80A32qbBZE7q7(7MJbhI!+A$ZGcG0eKJwWwf6ZU~^gr^y z{>R@k-QN>Ssp&MH(RosExM)B|te$x{1bj4a|1j(rqLtEHY{|~Jaz437EZuY}+!$>yhx?^@lXa-leH07kCkGK>bQ zxe#pLsnW2lG|i+szJ_5mFb11fWiI$35@aBh0o208Pw7Y;13@4T0|l6akaa@B$PhgK z{%Czr)Ka)M(@EPLaBpU;^r^2lXsOR?&x{spO0DR&0Uo6S(?*!MoqPR!O+~poo;anM z8?3CXlVW?BHmBR=`CAX{HCeuIYN`#MnhsJ|COPj{*DA>v;CUUSY>kVY(+q4}Qmdq@ zmCvN$MG4LmyyIpR_LFkVGYQCx;m~z(Gb%QZcJaLWRE&clc(%LoIRayAPtgJ|R(kN) z5d&{pk>B>^ZvU;S zxVHGH)xjeq>^YI;uSsrUXq)@rE)dIoa0U_pl z4+3nTu7>_Di`LhZ?;>R8DWmMMUl)+*k+vT5j?@F*4!hD)6MA3UrX*w&> zUdT>x84jNc)|vu8Ht%oR&q=NQ=UO*Zl3Kp91fgLCr*b6?;_(c^8EIOAv&u+9rmCQ;)poQYmVLs$U=-u z-E_P|ydc3LG2nwky(jq=`rMRaMJ3gAH%B%57C72fZ?D$<&!XeBmf4a8Dpsk{0m@3b z?qxgY`?aZ6VAvA$wcobqH3PVfr{%L-U$f7@S7%@cR7J1dqtunR-a~7@d6#>#w*LBy z=eGj@`n9n=Ln|Gaa%J#$4i|(lRHkvs)jJIVA%ubXbmIB*=jeSQiJcdC*^DelU_-#;L3n2Hw^n9@ZyL6hBCfja!L^;rz2o%g?Emi21JNBa^;Ft zF%BRXBhz#u=fv$WFx?&atDpY`|MSm(&S(GM|HkFSP-~$3A%YZ6BtjV2k4A9kLycC6 z0ldvU@qwg3$_v5f3Q(z4KuS%WaAK80oHyb%sUTshpiGSMyie71f(GztHUrkVI%ZRb zWb4Oin$TL_v%PQUef4^Sc=O)77?4u%F%ZU@5)^!O^p*-F)G~w4!@yEO1hF!u5-X69 zlR3XqsbkcGGcwXK?n!ATi6=Qv@dY=Gl!}sQ&Ws^4`3xzq=3O%xg-U$*i2f)v{ihmQ zdXAz}RjBV&L@Y|8r5|Vl=NJ99-!oTEEQF~Pj`Kv3fh+~2?POIq>*h{>ZV#?4?QOw$ zWpUcI2F+opXfflh*Qt9@(g0yw;I{30?~SAu#d#}ndDdY#g8*biGfp5lj~9qR*hNo} zdrQfYR<2ZP)e{GpQ&Y(+1+`x<4LrXjTkLiHtsAjoHA|l&&6H(vemQLYc7SCJt@WJ? z{q717bG9Gsxw^A?Omz9b^XKvoNW{C^7iw?dQq1sbaVqsY=|8=hT(STDxkV7XTMEFF z`h7JZxu*Za4nGO*Z4I|63_Ajp_CqlzssA zp)RY*)T1c>jO5+S$?rn04Vd@xFI9TagLi@#*!jp9l-+KJ_`sL1zT|M4(BMhAfJ6)L z%@qJt3zuz-_uD9sX_`QU)A7XZ&4&wXb*7>G8>Ne;h{zHi{unXNc5P`n_>Iwe>y_!j z0zr+{y8|rO9vmx?mCA{EnkcQ^jyt6RyRDp8GoqYx#9>&k_J-%rp7DD3iqmwWXkt!@ z{V*^LdtQI}iu=QXR0_MB8*6unxHu4o5e);mC=!AZ_r*vU2jVc|$4K$UfF43z%KW(s z6c`EMIlZhYHcgT3#~5JG(&f&JocG)9KDCU8D*$6(R&MLr+l&qaXu44R?O1vYa5InG zI%=*rAe)D%jfX9>v&ecse2Q~t<$dqKs%G#vf`Gp|9l@bB($Z?+MgMhYIZOb~_HQUtN%weO>A8uZ4WIR4`Z0=^s77(m-76!^gt+ zg?UOGPZK!@N|72-(8>Ywq`GsVn?50nk?#a~Ec~y#|HkpqgP09qc=X=7wba^ypb zdf}8ZVrcr6a~pWEUVoN#lk02WVMBUk#j+{ z`nbC_wdLL}My~hxabUOG;bUN$XEMtE_NE#bwin#&Z%Hp-fe7;fVMhl5T1 zZ~+c?2c~DU#aZWskeN{IqF%ot+#PNpWB9wv)17X+^ z_j`uj4i{@GKm`DNh)_xXq?T3lKH@_y&)5L~=XbVF^11eYrTgtt*qYEYN%&nLx~g3rqF?!@2z)nD=7e)cmy{fGYp z$CIU7dLgBm{mmXh@q;G>XY(>dB*q#6PQpVdWXmIgs*xNzt3oo4JErNxoKgkf&IVzt($K1yeDLA}ULB4P&W$&bYig_(^9g(B zh`~q}`}wildu!uS0KhR#iPP~!&O0j!SSvQ@qPRbNe?9PfErQ|N@!P1KM|D~~0|47O zy?~^Toi7I7NF~elB6z3V41s6+foaZ6xq8~G{n9_LvL+k7=*uhn=~MC;0PwC6z3=6{ zY~@J+;IRq&v=~c&1r2dsP)4cf?UIF}XCR=BbJfQefR;gX<)&){fU87NEV+;k1b730 z@Qq|^{P!d$Pa%L$_U#i3$Tj1C{rM>b^18zReU)$G-d6qphRr=>AU)LnyUdxVp7E{5 z&%26TDhSs$8@l|QckOO+xTiq!Ka^oiUM(njN?d*(Tp)^fpDtTf)9c58JYxKaG1%h= zT-cFIpm?b~4g<1(!L#50*Mt`zkSc>Y+0q-MI0FT8qUF0v@Tit09nqJWSPGy1@}Ky{ zpZz)i{m=gkhtEG}#2Epg7l@H@zhf99P}uDU;};P@LckBB8PVQbWJ~Z1@n?uu{x25; zOB7|!3Fm5s3g?aAqI#vBH}b-i6D1eC5BO^A_XMM7AkhH1W<2+)8@cA_Q;aOIpfwW6 zTRwlA7vAAQAZrbruHc%h-)n8JMQAhv(^3q?>E}~>hb_EaQF6+b-ryV(9Ik|AE~T6o z4QrZai!^A@2p#w(G#9VXfD^0w(vO&atk$z?YES)LYh;%bVhD`U6Jwwhi*h-gPD?*- z=fE{_BFuAQnzI@FhFC${QveIEKhMiP0;%t}>ubp+*zz{|Pmh3H134_p;|vD0Q>A^^ z0_sI@QmjBsu}T2X_XCGRq9g;Aik{O=wY&Z{?e9&1j*I7Dy|15t?-Kw_pFq2{{kI$s z`zF`_KHC#d%I4O8r=R5kc@zM6N|}C2+7LFZmIVxGz+t1UYsn?&oS1h0<$|z6`H)2c zy>jz40zj69qD`e{{1@JB5^cRJeHdGBF!kv5R>A2NuB%F1cfE%cly;r#3+bg|zkF_} z%OltLre*c~R}|Fkc>5;r=UQ7&kM7I&_$Jp_Jan5)_ObKdYV&J%_vrjI5a`+%dvjT% z>ODc2jZf{yv6@$88Nad}Uf-%*8#~uF=eKENE02xW?fepF$+(g;LSi30Kltzk`*GxO zI51_H)iD>LsClWpb2xF7oR<8)tx>jI58?iB517KU8+VN3Xv$a1KH3=I&0``WHP7}j z15#HW*OhT!Y&G9{=vKcAlzCn}(yGeobXq`!QX>UQ(ONdlR%j#fDp$kHd&M<0rhM;- zWCl7axDf`0N?rz(vagrywy}DZf}f{mxrjfUn+vR|(U= z_#(tDWL>R)ot#OR>de0CeOds6jS=6!PgCIatPEe-iym6HmNm}V4&1-eJ?$EWU1a>^vpZ3ogZzj$~Bk2H%hi-g-d0ND}aTo)`RA=kuwaDoXwa; z1iYi5WVPDxnls1K375_5+p1%X0i(l}wpZ-Ta{ontGayT$31gH4jz$``(7@`MJ zLU5K!Q_}&|8A)CUfiMns9Wk%^5Inn^Tbv6Qm5y5fW-XvLhAonbrDA~V-tWQ*qPn!T zeBVU}!8sjj;h<3C{t)lX8RVMbEDOkJ@5C{LK&X*7DQD(+qUFptnWl-9(vlX?M<=v3 z?7S8kkzUH#q9eRVLLp6)RWxvdmO=~>R}@#*I}sx_^dTT~qG_RuvGn)r_O;z;nG;AQ zvXjN2bdU3=b#o_z~4oqu3azn1GaF*CHHQu*uO0cB|j=%c@Pf6JU{3v7RHUq>EMzP5jM_@#l;|6SxT$~wgJApqb_(bM*Ms^=D&?{8VS=g=-lMP1bC*b42PQZu2loINsMinStj0j9~=4OAxe19Y}qf#iT zkf#huDan8UL70v+c`CRY!HGhd5wY;^yW^3={fs&x?jpOJ9kBo6ojJRMPRq+ADq0WI1ARfdo5hUN1nBlNiR=>_3(hRq) zVckBrTI}D1^d827KxZ5*x%Jg}*p8d_+ZL342>4izQ8Nm}b^B~<*AF!U#|-3xrQ*yr zl_mPPN^$A;X|0OTl);ZXCg(YZk>udn2Osj`PyP*Id~V*ylozjaL5v((#oUsW^gTMy ze0Sj14}9_KFZiQB`Xm1Sum76YpM8#>6Pji-?0ujN&J6w-7=}m)4slpCL#)vj-kS%# zRjsH7vaLIadi&SYmC1g@+rL9`tC1_Fp%b3!y(j8v^e(>2mHm$Hn5Jh{2KSw*f|y3_d{-&E@8UH;e7RGhfQFm3DX>2zB9?hIr+Tmsa_ry}Ax9!}(} zRLlicK}$8P-Fcp@_5sxe;AzkHCdU7{PtIW5wjT-L{3rAd?FR4 z6+wh|v>V>bd-qwnRtyq9lZo{GpQYKCXpg7>98cmE^=-)G=PIQBN#@ZGCAWxECO0=e6q@ z)E5sufjkVnPAAfwIh~HA*;v#yCr+o#e8^}4 z7b5%po)13ykY^vhAdUk`;WQ__^LXzF-r<|U@0+L4Tb*<-IAmp)=d z)saOO&vla}(o&V2lXd=pY1;rvNKv>+(K z8G+`0@SIN50wljNf%toQFTVrwBmi&`cJno4<)xSNXGO_weN11Q{hEThS|AiFMfK2q ziK4a0sG=073-F+VDc+CW_my_n3c{7LYW@9f6prme*np5el<_hQ=yFRqFRT_Ws&(6I zVPxn3z1jPEFa+thEUJ$uXuQcnyhW8S+IZ~TbJEDw^S76Os{gjXKGoZH^@kr)?4q0R z>bt!1{@;53)_JoHlRUT6JiyH}pmoeYe&0*;tjjoF`~B%#d@KT{#s2i4-}VaYYd!Ye zMgOW=Ij)kJ(vfE`ZixFGuaB9Rsc>J)QZ}{a_gZacq4+iU*I(nTObhd@eDURL{?)(V z;deXyIGR^O%_HSI5B;V0|FX}TvYT^W*sB+1J!i_Ctn^jwzkDS;zvN8uo}iV7zze08 z>2jzM!D@z*DQP0lGx>O;q;&Rvi>uFZN$j?SaEg%TncZ$jnr17jS3QD02E+&RTGbST zlrzIPl2c~?{Fb}>m$)LNRLE&&K21!=6FJQY8PyZ7?`F#E`RrGpl8+h5j%k`Jv%he2 zJMhsDKH}Mn4{$y*XG=W@jeH9+GC$|>E)cxO`A~@<&Fin?mvj-EKjxh)1yszaabRTv zt-c##^y3S&xpTF&Zr$4+if%h&H}(CO%MzcDg{rRh3SEQXTW_ED9@;x|{c76I-K-LI zj9%LhR|ffV{mXH^O%v&(Qnt_5jJn>qYhz7S&t7BzvH9dwj6g01oU3UTrQ0P+A;!Vx zu@^)g_PcSppSEw6%AGI_v0DQZL#XK&Ipdu_ z@2=X$l0Z?L-{P3&iBL;@s!NQ+K%ORo2>4XF0TMYSybl=9Rn>G`k@qGKOyYhID0LnM zX(g!(-dWU{lvXGdya>^oy6?Jk+v*|$S>()1Am-sOPB_h(IYTbK0&Wg*jcjqf2fm5% zzdHWPA5L)MmpR@)_u5HzeeZ9_xyq?yx_Dl6m6rS)>KfKkTh!Se?9@tX4!*D(3PV(8 zDTtm+u^Ra;^swjlb-Ar>c4b|CRKKOAan*5(*)!+9dOLmYVy|cYzOLTiWBS>Z-@b1) z#5>7$K5pNZew)!L-;F#906gX4dRwvOa#=N&2sY_{r*24_z=7p{R%mkfds~dx_gqcl zT2B+v6-%zDyC^mC*z;W~&>KX%QzR~o#fK2kx@qc_b1sa#%{9&V@9TW80f2KKTtDBy z5mw)Qry`di&vtWogGw+DH-BwG+ipJ6AHTM_oSpDUo7Zz>yPwY}a~t-L+|O1@Qui$^gveaw}@KZ z>b9{~XN2!u0pm?a-1W{sVC%t6Wy!V+{M+5YRGqdf8R-yk{^%T+CF9e}5uXV#17H^x zvz&O>2w}!)JoR)ulLnNT@$sjhAs)|Bh=GtGS`Db&)}{TqfYtr?>sToTbDkRrFwZk2 zXRyN7nhqS;j*pt3Ylf)PLDh2pCC?xzhQL4|2V(6U^ORAhj3NZ$0Vx@4qKOW6YR}A! zd7Lnu9r4BIFY)N9iTn-cXMnmAc8uVxD7xqfWGM(kM41HXaD)JuF`?v)X}m(7M&x<& zvAxBzGI0^@vXYybt0Dd6~Mz{QhC7>?&q4o3B5b9@6c5*xiAa7uasD7tv9 zn)9#3Eo!LEx|5a?V5JWGd}uT4wTrkB(CF6oR@=^q)dJMsJDoVts=CT~-ixBI0k>j~ zyuXj$d1hX~31S=5ZE>yF>wo9tuNu)sq?L~A$8H@1R%Vh~#I}CFmT=o+H0*(eJ!sJ1 z-zpMZ`|ER4p(w@nCWQ%b*7Hdzh{Tx1*2HNtfxE-uykqMXu#b^EPauv!7@+YC;p{P< zfBi3T{`h^2bGEX6-WL^%a;Nrw6eTM#X8h;R{~iDI*MEbTzxf#Y@)AiE3;_)Wz7VU| zwI>(vOGq&x1!ozppb#Js zWCgA=ki{T7a-ad-eAxro>c97Fj~d*mmBJc0$lTT?hyj%SdF#2vQ%|<@x!2$dYzqG- z#wl*=6;$;x0)rJO1V}KwFjfT>K_W)t8Hbc{JObktFgGDYwSA3bWj$#3|JrzCPON|s z+}vs}dwah6GFoTVEl%i-_UWc=L4fL?-L?Hue2914h}M?a>i!7#9t8MDzDp;`!g-{juA-A02PjbW0liGI#bX z+3bZ_t#MQ5zno`G^W=^-S`Ot+DEzf&_wRlD$tR0L!{p*lE~>f%THe&w2GxFaC4_*v zcz*uIiR>e(BH|^$?EAG`*fdR;<{46?(RpKxD8U?PRc%V+_1aaXd!-c4glQZx91eK- z`AZxw&QRuTjy2#F5SED)3TI^M;#vfGo)LzK zvnP-6=>2CnK7I^@fLX~D{yj33i$bL!4W?BPQi3*T*mM4A`9q%8fItoiEdAS3pzFJO zmDU@{_1}Gdb)Tlz3omZJU&nztPuCmyd)Ked!gb$aW~&ag1F~=LfwYVEb@$YRUOkwx zR~m3@AlETmL4jW2u7c*oL4$d=+VN91fxsH!o@<#&PpctS{~YE7$AlLjeu!u9 zKezRPV2R1CynT((nj;#gjNkwE6MXNxe~(}M+s`1ESHRVXq=H!l92F^<^ImiLLyT62 zGB5%Kqru16N>UOt;Q9UF(8i>JIAj_D4o2DhHl=xZ6_~Bwv za8)sZN3qHWOaxX-1sTUHh$@CO7&D?5Ee(vw3~{=@DbstZ6f>Y$txKG_yY)u#iNGJ$(bZRp&@P)sAL)N+5AC6SnHrJhBPV7YhM_qPA{OaM zDFU7G`>F<#xcJ{EAAf>rnpS19h?Wv>w;4#a^-*8@v)l*Z4v14}EpPgCJ?o>uAre3l zDW#@D-AD6OeqveUzPv%~2JUVk-GkfQf*>0T<8S!#6(sCJ+OP034!G?a89GC<+$S zv-th*euv-u;v@X{Z~qbh^WVQj9!Hev3dtqUQwRt{1O<26V?-Ph5NwWx6p`Y9Aq_6> z7aQY_*z(!I!SeqD157?%tK%eX{AR8C2nM)q85X^eUjM$UzDNc2>ho@EmMV&0mqYN> z5N=U>!V)buCh3yuBdSOtS@hq<{jylDhOGxPlir_X_amcn8+bhLXioR0{|ih5YzzFO}*U$aeDyV8?_zq@uoelj>Wb| zy3;@vFyEXnFk^_o5CwrVf{*=Lt3~y6{d#B*?LM`82LNo14Xul{{cGhA_tY~55Zquv zJ9a-BaQDuf{%S8`*7y5OX)@albbF@Nv)LFddrfzud=G8Eos5qghRa=;9=H8&3-Fwd z&v*RJ-1xk%T{m5}M)Pv~mus`EQSR&7>-2a1{>({BX9?#Q7Z5QAc$y}t=g0Tu(_U5a z*oM6znCBTh8m+53 z@zwbO`V9s0y1Ke*l>qwnsYTzZM(B+GT#u`LY-;bN;Nr}p{%6M{rg^qPRW*Xk0wO+f z`kG;?mS9u+H$`_AOM&D9nP%uXV<4lFj#pQhRS*sX&YwNS#q;Ngj~{`~&Jj6)Ljss! zG7eUviit2Bj)-Y6%038`2CE|&A9ZSdO{gG2c4l zJrLbLqyCsJ%~w^e&o~7a_8j@$@wamy_5E)}*S7b!l#4t~z3ZLw8etxWVb`gz*R0O_ z_F8>9L4W=_XSX7XRp^gHF#0|dVqg@t=)XnJ3q%M-6`6pU2;cbDw-C?Ikrjw3VJsQM zjG!=mj4X(v`0Tg;hky9H@8Y8${}khIKY?n2<{62B!w{TlV;U9g(ftskGXR3sqL0C% z{ABx#s0rCs7637Wqv>dnA16L0Vv5a4UX4ex^oY7=RIsnUgYA1}=2jh~jQzhJK#ZTY>b?<=^b#u zG)+zGz#u>M^DS(eoC^p(HXQT~K2Ajy^E9uh?05D5*C1Ne52)09ZjpaG!1n>qXXa3W z0d-~oLzN*?9sjC}usk;@;%mM!0P6S;eT4ms=YV)hswd*fmI`E|eBGxD!9!msP|O&|pA?8i4bD}Ri3dSAC25U+b~ z%UPusF`wrVmseI|>UeyDEbDdPu0~XCCPHSBh-sQEZ&lTlV5b2d>?sfnwYQdq6EELapIvqQVoXReW9}Tl8bz#?0b1HX z&WI^l6wdQ(r>iS7)T1Co^=Gv>Xe~nAIgr+8r`rcMHo-I@k24s+%a<=Ps{lj9@Z$XHUT)LW(_i<_O{dC?N(yh!(MA4pstC37iIuq zU|#l=>g2XaDy`>sKL!_YYaPMg&hH+$*;_mM<9c8~Jet}&ZTIZk8P<1HLB7NB99gcGdx`<< zIoC>-A_BE&I}Zb%y?B94?rhHmAuvidZIF-^I?njnzx*qH^8J6tuRi)E`0^4#CO{QK z0Kf_kVh(=@NJI28f5XB8sQLbE?35T%qy1}*w!2eIF?eN)#o1>U0t6X#nsk|)_Pf#m z1o*rUu5r@8(;n>W=VhOAQCXc*MKOInC9jxb=d*E;^W3xp?7kw10(_X-{eZl5pOyRr zRj{x|$9eVsOHqhs15DIF64P@yH1X4Enjn5}*@aFI$0JgVI6pswjI)*hl!B|Pt2S>f z3h!W48`pP30G4qFzlRC}Ec3$7CSHk35yHa6Ks&ZpTnHdih}b=-ngDFCfi~ydQp=kC zX`NZE4SQhOZTnikZ}iSSgPakzW{>QxQ7cfS%~zbv;nRzE;9qejfCCGG87u`UGLA#S z7h}O(iUG7lD0yw2-NpXC?%wZX+uEyNg|_ye-gyY_ows`h0BoCT-5k2sy&v{JhV1#r z4&X6bYTdnRYn1f*;i%G0=xcD$l7P>NtUBygaW@l_+g z%$9?C-FeOEK6UV_voy{VuEr~K`j@bf8EPtKz+!EH)qeC^0KL=RN0m>(Cp20)XvszG zbFzC1zlvD-z90y}qMR8D$=QVc1PG2-91e%ZkZ5Juus9;66a$LgVHwLM=4tkORg97$ zk&!Nr_~2XL#5e!yJGl7T2S|?|`+3cvRso<4=710YbqeutJOf7)Xbf>cxV*%1IGPZc zDmYq71hJKcOflkkJT$;%?;Y+rq5X5+M%1p0$X?g31EpHj9ZS@Fk21e?s(NQVgy14q zr`N21_kh&iGYD+D47H43{a!u)8Z6h<`0gFfo_@aP#B4zT0IihTw!f9RP_JQ*; zc-5cw?o4>?_~bFd*#X6QiL8Lm1;`nRBR>80uko`V{0Kk&{tt2a>F*I|tD;e~m~)pR zh!w*SaX4Dq4aW#DBMz45UxB6&4FDkGMrS8?wwX*MP^;0zPJ3=YR~A4*aJqjLr>wz$ zi`)_@FCvZoyx)R5J+RWhLjVwW_Tiw3Uj~4s9MtnfG9aU0e^d$NctU-r0m~dw7|Woz zGt1Rsgj-f@X$DmFIs7;Z0f#dO?|htD+K82RH6}$sh|$uDa>n2q1vTZUj1%6p)hKoY zc!>}Kf!W?;8Y&yW=)8rJ<#VCGNG+otz1)0=5@3W zr(kT)TBy!i&mbs8FijH@v_<~|6EsGnThAFq7DKZt#P<$H1t+S*6JkIN79pPI2|){T zE)W9Z`4P`wd>tSD<+t(Pi$6np`~)~0jUqc*Bvy;jwPRvL0*HZ>2JkR|LjqI4;dq1) z;o|H9DnOBfkSu**o@ZncWGRp`w=|3E*gAb=wRfm_LoCl1-^38`7!4WtN0}ey--2DiIfaDB-(4_;I({2RvO8ZxL+{N?z*`03#c&z`` zxdknvfAy`Kpy{GH5{J}4qB__5_q!d_J#f>5iERwv)Y@6D&CNlNwW5YNn5byNg)5O# zclMi@qq9I713)?|yb3s4l>xy5daycpL@*ZtX@|1^py44Zi+NXB`w6|-< z7F25`!{5TQy0tX`dI4032|)=%3OF8tsR;6%!K5%j#5HJnpCkGY?W@=B0RXTzT32N$ zR9dY~B67!`n!{gHpUjAIl)KtI4hU>(S?cC@+cF+Ewe9A%jqYuuHT`G1=K~r?OY)b@h&w zR-L38#_2wP)eU*oXo=A6fo81m>A4tMn-1LA|5r5-Ugw$LG=1sDPv89dS~6lJgm}QL z0iVwRLO>=mK`ACHb#zVC#ICm=VIU$XWoQH*J$;N1zV>w}MSy}^%3kJ(!YZQC?p$4D zE>yax)rA&SE8pcc&dty_>c7DLrrJjZNUc~U0E@_1#sd@}hGal;0j0r=ZMC$7EX8Yf z+o_nWY@SAj3M0;h92q4?NX|yb6*FuZ0f8*yrZ9S{jw1>I!|{MaOqfc>ECut`43Yv? z0ee1f?Kc(ia*3ucE-HX#P?^B<2+A|^R50clhyiC$9^u7zzK!?(`~||JN0^Ba6rd0h z2Qvnd$RgA3_@o#RT!|fQ`PV`Kr-+$~O{z$s#BD=58_rNl!8j6P4A7e6PJ}$qAY!ju z&hTHiHNB3Z>WKB@r3X}OzqBG_Oxy&q+VNPTq(o$@*M1RFtCek{V+x2sN``h$G>E*E zoJCKYvriULHo(QM)aJ{^`QJ+cSfz5PIxXBp>KIGZAN9E6U=#>t&i1$+;L##h@=ny5 zf%ab@W6r=+1@`7%@2FI8s`k646G)zsQi7&hKRrUBD56%wo&t~+&)$EE5P3>e zne3UM=HFX?1Oj+k6F>mRMYPV9>M2s>cAsa&s3%jImt36iLpPT8y*f_LZ zOkgadAc{zeV+)pOUi?G;UyzHimWUq2>lpPV=^U+{6^F8}}l z9Z5t%RLXV}L*i{PJKpvAZ)kmv46ykbxDO=){~rrTGa;BM!lnQK002ovPDHLkV1oPw Bor(Ye literal 0 HcmV?d00001 diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b377442e3c5b0e9f5faccf89ed05de1f7a47f1f0 GIT binary patch literal 129190 zcmagFWk4KFvj(~g1b250!QI`0ySux)y95YMu;3OTL4s>=cX!v|?y$gJ-tXu==iK{) z-JPlKuIiGfpXq77Dk(@J!Q;aN001Ousn03^00j6a1OOHq{B`3s`vd?$HCn4_xoOGE z@tQi?Ga8#YnwT?s**k&d006&`my@xnt+^YiiMgeo0av?!@elK3|0QTl?#-v{Mb`Gw*UIOI*jLQrD{eGK?ob;b2 zZngsCTJlPyVva86q#TSKjLhVM@TB}MW){3EpC$gSGx$n?+{(?(iI<7V)6yv>!PVQr&De{? za&oYo^xpZTetGFuhBdSQwd^{%dnLYm5J*`MczQGmjso7>y}w`sV#iF<$@@o!H3Z#MnA0#`L}Cvzqh zb5}=q7gKX_4|4}Mihn%u?iXGqYcF#<_@l4?+TD}u(1BWq}u%u(kusTva4RNu|ZbI9Yf& zIT=_P|D^#OI9_mHYYXqs#%|zNtjys2WaeOCVOL{e=VjsMW#^`2X60pO{%7QWT7&Ds z%-GHN|F8XfR+I9Bd&tZ4N?E(QIl6fNbL(HJu4?Z5ud9Du+FAdT>QdXs|7$J)}t+=Y~dk%gI& zEDcFdM~_xw~n9bf7s9ePlNwbso;M9x(7x8FqAO;2bO>@{sWE7 z9l#Le0>+xeq=5_o02piyj?Djv2mnBx=$<%1_gNWpKnNPGhZ-9jo5}b~e|&GhNw1Pt zObKYY)tk*xVk~CCmtsg`NkQ3UXCb5`sG>$AN@_@DzBl}k$I-&(qpQPy&cw^Quflx} z0x?HE)hWEtM^$S&OhjteYXq4&gp}ExqSOHS#<5@NEGggaycN?49RIB96ivL?)+hd~ z5Yw)|sB+M;-g4d=s;@h|igQXLULfzb?9NWrQ+gPm>+ue7=SkgnOk?0+hsc38ro4;p z>ClSTl{WU~u^;L!HE4=|Mt5ZA;G?KkZ~c&oDgr?*$2<%RdBgni8lZnO%|Iu!`|SES zQ!e$Jv37_Td!#oqsaSPZ{TFeHRU~mrqS4yv+eZ7cgrQ@53l|_|BJO$}S$!p8jC*_` zamOq^9|Xs!+6PJz!v&!VsLn1UcQ(T9Yl<_pBH-vwf~;&(QR{#J3r z1DORBfK~tjW8O|5z#t|ZC%>|zGaA@(?!}_xi#BN(6BiyHh_q>X@PMoHj2jdWe9*Ez zHEr|!^NIx^Ic^IqufLmGf0hLJ{j9$u0~~H(82aB!=GqdUR`nq7hu-;NfWJLiTW=~l z9z^r}Atr_3D*VZV6h#N9AiQ9smANPa^D9Q`3w@tGtl+M9+Sfg4&jHKW$iG_)?2y{- z-5BmWM#n|YUqUPD2Y{_fuWkEZE8Cw99Z`%Th&;R+j64Azi(A@ZY3;Xf0>BHGyg>9x zLAXvT#)XH^dH(45f4Hq`0mUBQVJxyPb$glpFs0PHe0D5jSj!&a2elEN)Ez}r1RkO!KQsm_{8%U;cmmuQ%TGq7_cJO9zavlV>)IEbKYo4e}!b`ySw7v1HU3%pUhkWp{ZkGcAx zQup;QBE1Gb|AN&@42YdgX-Q`s7OERBZUTOkFoYmfVkw$KtU50M1^*Q;$di4OsO{bp z@X(R_aP;SG49TMo9EFc3$eqZ1cAbzL*NO8wzUJpI*8o4_potF`)<~z~L}iz!CVpc| zDP?qDZgvp?UD5#2S;qxg7Ch}`Y+Uj526Pn`M(RnQd%+N(({|Es1G00V?L}& z0ztWe0OU?ZhzAZb&3bedpvW^%oE#$l zaBU{Se0C})_A}xh`0&ePI-n-^HrEE}oZEk_NI(|sPP+F=db0Ch2#_=*Zn_#llRVe~ zaGDjEYY1o@xa`C+h3*!cE3`J6Ph-LH!H`l2vVf|az$5ZzhB5ZQ&U1kpM{>AmBRwyG z3d{p`G67#KgYJ@uUP=I(T9# znMQ7`P}-EdACdy1a@;|6^$~nTo^2sg1h5ZVM5wz{L-*CdAWK;)<(kbS>RYH9KOL%+ zS-hA2C20cX65um3EmgmCHU!&Kz|*?&%F997I`ZZ0EJb0tXzya1%*isx-_q0p^FR*o=F6WQ&5r*nuF zw;Dp$fZ)%d=Z@zdPp(Z_-}5(T+fxMKD$E6T!;j=8?t^QBFxn$|-;J4Tpxz)#%wOk` zm)n2_!f{h76Y;(cbR)?w>+N`;bth34u0jvp7}hCXgIBG)!Y1{kkHf`|g0O!UdCoa? zcKzOE#MA`^;FU9ogtyJ-(vV1U6`60fwTchGw_0=2=^6N7a{dynYil?cyaC*#Oo7Yp z_Kto1;c!d4S>CGfW{SJ*B4$wTus&)KxEn;C^4#_eCn_uXLsuF%l)-8QHP%;gfc~Z| z_%>Cz{+4tf!Yglz55Iyg!l*CNg-%Wkm&}sjLK)yF)&oxCl@d*+SC^qMB9EpaV=$mK z6>YvY`W^h9UoPXjNI7_s)wRW$HaN>aV8KJBU9pk;WhQ{jeO^i_*0u=&TJ8M%dLKso z^@4~>XvP&@5{>Y%Oj?`dA@u_ncTl?*QNqZnkAj{%DYHAP&3y`|GoV1I-T_JO8-vep zF)KgxP88sS;LYpZ%!1zX8|*Z4BjS7ga0#rwrJ}Uqj*zuieztvs^Xb4uge|&`m!Xio z(G;xAb3*-=KlkFnd6jH1ev_zP+CbYR^D&5CLvaNdAeC@%yQS2-vCvSYT+slZp*0nf zIe_b1;pjJ2gKzQGzZeZRl_<2t6w1#*^ril3M}b!N_@x74c?0SR?2GD|GMqs3&<8NM zum9ryj=&5uE4*fgKU}iPgD6 zTsB?HNx^ibL1laKA08WeHD(2BD^F09>$a!^)&@yXdB3y3S@L-uhay8t*|J^vtrirp zaHDR^0Z_YaI$&=|pe}?U9=?JNxfimj>yd|fF9G=v_Z@p;U}p&c0)&DLJ*$A{x8w*n zNcJ#l6Qyl$LmHj)5uwvVzIm`eEYF4|GW25w^pAm^M-0JN6cR<3RICs)2(NYF0Up>5 za3{7Qs_lfQW*p;}2*Ie^RN9!8_(&xbo4#1{zUfbTy(U;?a7>5X= z9EA7s&+%+}V<36r&I=@)d<6tCeYgnO8*+XgMr4CzWn+{yhb(mi@vuScl{mRa8_j+? zBHg2Ci=Suam|d2eR1Z`&x~m9EfUsmm=9Y@c^Fc6reg$amB%il7tJ8ak7c5Ef z7RgS#=+4beoCAqIt$!P zh+OYzf(*>#@Iwnq$`i0e+v6d3cs*J{f0)~ciW`Pi`?W!_fz7N_Zdelgd}qAR%8|1Z z6NExPCygA!-{2p(M!Y#2D0DpQhuhQpCviJ&AxS=cX-nvfDJvjJ%nPeQM3(t(i3~Q* zG?=WNSIb`RbZ-njw>$Hh0TDdY*f_ez<7TYdYBvJSI}l3)LZe!!HL zq_Gmqwcc@_;tVZWcT6!WQ4m%6(k|mXaTm-bT=)|8>h~!KsjM*%t>V6jqRPZAloRod zo%CGlc7VcJ!S6(%ZbBkXq=6BB(T8tSFHq8moxb72_jd8$-7_|sq;)zr@j88+BIY+J z7BKihuK{&;ya1G3gXW9G(=IUMnu&OaPwg+y>!4mL;KL5m>Am|VIsY0%q8oD^rm{}< zlCglyHPw!?9q^(obmgfj5`>XGAjW&c$wv6ul>H;Vx*~myCf(WIW(G0tO?w|ut8{+? zHq8@-G&Wzk0xxO-eiT8K0H<#?%fiO1p9!^Ds7zgc9DK{qZM*W2 zvoH$hn`~B3K@!|<4~q^aL1fhoW~Jfp zPQnX)capT&vk72haVl^`Wm(kd05t}VK&5o0SIg@LDNEK(S8W2C#@PT5a}D~t{sv(8 z95TickQ~UB$1V;uB~;(`<$U#)%#Zv%q@8X)J@PCZw>>ut%8HmvkKI|vroI?}!yy+Z zKOu||8ghh-IR$hpriVR^N^_howWYlXu!B4gyjBk95T3a{cz8*?Gg+@?r?*TYFHOm} ziOEZ<$%~<#A&@Emg}?Kx1Qhn8?FW@vke^A`sqkh&n(LFH5I*~wO_il=VG9V_=x!`% z#}G%)T3M{jL%tLN&6@zg60$amI0}P9SCY)t!&AdMsL<6QmNzcT5_qVz`PAVQXOtJh z>$rPU z$uJi_SLkW{L_&VmbFxJLPuJcY$Hy+b67#->%i&E#M_MlKffaZJB#!tXTyx-ZeS;K^*%I)I zdVL%%j59OB8`ogp+4bU?!o@{K_CNEe^SJsf`Q|wJ+~X@@6mi}C?n3s}`$Y^Uh^KSN z3kIvxJsZQQ&(sC*F|=;queL+E$%^EUl4~Kc?20aw3G|s@T(E^N`snX^$&?^n<9b4L zZ^AuqQ6yS1js;zzo1}9De>LE5C8atCO>d{nl@lgeP3~TICEhK0Vzml6RHxJY#09WS zqkEGC4a=p$0F&61xM6Bz6q@Je05|uLr@BCX4{$ZSN=ABc!@uKdsMCgN2@})>hwyV{ z@aym|QB2dWL?I&?mf~+qXik_k3{hV%R|n@HM6^q{s`kf*sWTdCDo~*1$%;B$u?w?5 zQ*a5upw`e_@f>3%n{HJ)ZN%O=T?H!J7LT^c$UCfSSPZn{l4YXBK=4!xXfThVs=l=9~;9{#lI zA1;b{=6iCpGr4R#d1)E+dd#e9kuww%_;q5PrO}eG*+z|Tz3?|VVej*Flfv}ikJg#A zby~}y@U()Lql=9g!M@5{D)^3d2;5%6TVfEsZw*U#)w#lTamQR56kTMXsAj=wLSH)N ze0)Tq!EqKZWYPhzN5Wrw?yu2>91y-~elDwKcI%WQ%=f{NcBpy^F4WI~lNXjiJ-#*< z9 z1=0X;hp+)w=LNf`>wCQn#l|2W@avVS#&)lBjK3qtPskZ>a(k|u6IcNIEkS>W6aMyb zPD)o}i!LMX(6~>)M|Xx5zWs?!sQjYlVSXoAr0S0gtCW^cxin&jDqA_O@DVI0Pr4a44;9<#(9HU>e(Vaw&lI9O7bIJd{S}$Ih;#? z>TZw?N8>fz2Ac96_UQC&l6`pbX@k?1z|Q;?WM1W(yy35p5!4oth;f64kx08-z*50N z*NB61xMUJ^Ub7v()LQhHaxZSH*~pDd97~`BHDAJCCvHexWg)}s#mA(sISoZ>*Ih|7 zm=FrdJ(s1)SDSOo4L_gT?N=OFPcR|lBRccHxq+DMsII>o6aLfL-vO;mYYw2ATE)kk zOT;Ev-oi~@i}SPTMIuy6fH5+eMe!$o__^8>^V@ZjX#9f_?%q*SHz@my)DM-j#r=5t zBc$*m&fUFW@!b0Z(nZ_6^@t(Fxyq~ohLV0mI)9yXz*&jY@+`|LYEY1IMOp_4w-)L| zuh(5n>v_qGxP}Mj2iy2k64L1k-;-w+m=o->5H4-qg{=Xb#J1KYE4lLg&21kXfvx1w zzfa%xzl|0&Fm*u% z^m{u@K}Z6PAvdyt2^JCTy`awvTku_W#n$y7tFqIiXC9{Wz{LM8og zG8v-3r7E0!jaA-JCKjvccqxT0t*UvIZW_p<-=1mdFv;!5pht;Xtz7pDwR7rEjj@p3 zJ?5#95&1rB(w8i)=I$q?kFa@4<*wO+?@<<q#4uoL1U4}!)OY~_yB_tSfldlpKL zH2|sWzyw0Bn*_#4=i+ugyXfZIsB*a%#YA>O`>^Zf(tgn<3t+>=XRrG(?1adH?LC13 z-~@hx3#$BZffMX;@1;^Cv!cUUt(=;QZ22MMfF#c8F^60kTERL5%QA*q{Vgr)Kll2}|S+5IB(5$hCVUM%cI?QGi z!Fm?rvvLzaZh@MPvxz8y!&9M{!t7d%W*Rx~_8%u!u*d~NHKXd{AtO?^l#<1M7u-{L z<0OdaoRQ*YRh-mj+p{2bgH7 z6_%GqBDSw->dvRw^Wlzt6OLu=`~E?k2sI0#!GMMhbf{XlsAV)f zX?jRq15624&KU5ocPCefcf5r z#Muz@9OVx0^0tC_HEz>xbhG!I^Vv-r<`0)jq=aZAAd^P=M z#xQYyQ!eW4&nPwiErsP*61DEIKIf43UnVG@)Xl2%T5)E0QRuZ94J4wMT$3Sa(22M2 z+SXq%Wp9ez1cR6%#6<<*m6{aawCAUqB_Q=m3%e0=-Z2lSVVc}+Q8As-N;K)S@6SSg z#@w8ceOKtNNfCs7rPGmOT6-l;J2*n*S@>4-kKT)9-^JTn%$0Z{ZRJaN&7|jtb=z|a zq;3eJ(3V*JDIIW;{I_BAQgekDvz~QHpoc4=6rc^@Uh^NbD7C_h`yXf({XG0pYijdt zBeU7_LG@b5aIaU1*{4oPJ!gQ`Pfq6)BoE{ON2rS^d2`ZX@_^vV>4VF1;n)^wjJ~2o z16UNQDe6MI{NX_eEd=%56BMW9W%2DYr&^y+(4m+wC$?_&v1`n@Tuqb-Et4ma=D?3h z-OO(qLROgSkwSktK&OD;7VKCskRxtfQ<=E9pH*W)2RD=0fQ9ri!vl}&Rj8@Ln(Ug7 zqi>fHA=^l<08g%fZmdJq&j0U=d?`Qyd z@KSGeS~db{PHFeWRXtOW_zyQGyo70We(2v0+&Dl25#aRzX#poq1s@Jxz?s0nlvND-c-}Ly$-OhNMt?wMs4NQ~UrC9itzO<^RVosmn-GD5 zHHHWx>*+A}8d}uU4&L)95)(wlRIP05LTbXcDgZ|s*Y5RxyJBO&O^8;##OBv>ok5auHc1(b~pm60(d_hRY9Ia_=Pn}4d&BTwG7)aP;?BM zI$1mTYqcR-pRc6a=WofJ<6@58*WHsCZwnfI?x|^F%12#_Lh(_my6R~pJLRK9!D9;o z(KT10viL8V%IXxy6RbRTCj@3~RO!jeujg^GsnoV@8384!i9u&8@$-JW61IliY>y^f z7i$8R;-wvZ`x3^o1!W-%jyx;o=&9rStZRZ0vfcBBVH~b;{~5kit_M1M?>J-|a}*hB znQQ`-_JOP5B)tnel!fCVang8{YMemH@9eud*QlxagXt3g_JGBs86Xjxsg$tgGOHs{ z+xXI!^Tw^qMPnL6K!K3OxC}8j?DE<>ZmQ zB9E47)NTFtE0>o##-RnjOY2l z|Cf{=c}mThXQd^q@R$e2!>k zRK5>OX@fRn4QNMA(D@Un>1ES05?_fR!$#eoTsG+oX4!z!-o&OC&}(d_E{uf*!>zPT znMk4MZfYw~?8N@Ac^4|X?KLd~$T^&$>R35V6=z)+qwO$T$k7&T-6lbn?8swV^R3!= zL9yPLj~-%eskVQuMR>K*>yr`&^amvrW68yk#ozY08>|VxFN60;{TC`%Z$|mNl5qrCi-UXdU$GzIi3U{De zwPu9N@CzN|a&}$Zp7qz{W>dE4+bp}B`i?O)#P+&B?T3!tW2XD?+34Vp70+2`ZvJ>| zj zMqe3R#QLfynT2IjutZ*{%<5pc>>D^Bwvh8Adg7$olbAn&@hIE}&f9Ww`4JmU`UGSRRj(9OMn0V`$Gn7uklWxYS|mCc-yA!!E$g;14nMqH zv+^s&q+FJRVI1n)w_mxVv~j6}KiGXPhzA44(aST(@h>9i0d-iqIKJO`4NlH>`0Gf`Ld z{)2Yn1Yyt;b{CMU_{s00D*rN+e~WE2IE~T-KWobj1ubzbnds*_`uE$exRF$*Z(pOh zaYBV^pyqUmH5{CJ0wwcY`u`02kp;w>i)3^%B}ukw^fx(KnnoMf(gPbCsS-WEsJ_Mn z{2p)VJlhcXR@K1Vh6``?dhj`rkGCDC6Hlqz);8&qtRAJ$R7ThX z5|;U#R~V}zWy<~>xPJP}2YVtO5CsKvu~Acc=F-KjJSWsn%ZB@|)zK!P1#gt^bCZgM zF-rH;Pqsd)X1cVV88fWg7T(l|HdQpY3lLn9pQZNgqJChjVJTl(AFuX1gSXzr-1?M0 z{rg#fGlHRI3G_a~W2KF!qaNcc3j&4I$}k&!dr=R73bsNPcY05}+YQ}2p6cL!=wSR6*Xz%>@KJ^^F%Vtw0^SBlA+!D ziHSVMh4Wg^DRqxzET3q&0Rpoeu)Gb*ANTQqh00mMt~}7=-<2DEB>Dr7780uC@eo3^ zp^ZYvF^#GA$0s=)w_Jx5GL^GqDpd7Wy1IeiVV+fczOAT*5L=b83OqLYyu?<<%f9R< zhP^@>E;hmc>Gl|3t%$?Rh=S>B<8^F<+-uNENgedEh~NTdanuRL?o}d;7X%aKh=DOJ z^BvP((xxug0PUj)G|Qf$=X;RfLKXMmEDlX{FJ9y>G_}gyWyfV~Rdu;wG)7s;=fiA> z==v~-cV(oro2PbQv%;4!Cw#JVAnksh8tjjsjj&-3UZ>UoYJcYDmhG@s2;RNkqCl&c zoJ8bf2!8Az`=KoHGs&1;T7ztoj4&fWcyK=c^X$~(8fUaA;k}vxUFnCHgvc)j<6&E zSBrP}d$lM+UT^{YYH*6bNJZr$sYKb7RsdAtA%>&~3%^4^%)&F!!bTTlAR0g8iWbIn z;_6QSjpnFGgeno{$i7{hZy#Uvj`R={Vz+W~_C%|v8gq_z;id3v$p=&-BfV!Y&bdefdTioK$ax_N%)}syd zaMITjpQVG8jgS`8F!E!{BhdG^L%s5jYb~N^&HP4R$NSX-y7LIi^c>`5=@6WIeqiVU zeomYqIdG4#M7vu6>gnI8No&l_hRZ8 zsQiK#y0fn6a`Y`mu78=w zz^A?RXATXB2CA=ofQe}!y4t4Fu{|2S6gn015T*D|@D?YC*+|fpv*O0M;zI5lAU9>K zKF;p)IsGj_IGGDSat3!Kb|gDzBIZ#$Z^{ttk>I&_N3?^=8G9Zp~+8Gld5*=~XK(q55gTK^LuuPlcqYv8+eaUkUSN=W?E8Hhh-rz-43pM{fKmG&ZZQKj*P%kJ+(NYD5c3MLxhBHi$5j9 zVJq@ikrv8i_z$HzLs~IngD7|#|z)dN>=zyWo>$o91`KB zjQ^u4d50al1dO!PEzxzd4)nP_iWDBi8pQYY=xW{EF$0V&B4qxwnclqXA&?F#DW2x` zf-oClcjsOOW)_dt0QZ~2LE3)%zTsB|a&lhh*9rn3#h&Vn;h^PH3uSwdG!z+~R}S8t|4N zH9XWxv!(W5KK*r35vY5aGf6mwB@wB^0Lnm%P) z(>UHIaKatJR0=%WR`<9CUVZdv_Pe}rzJ2`evc9VZx(BWG-L8^zli1aky>@BMqXrt*S+I`P4^W!13anN zv7sB#-`f}fJ{nup&LqRQgM^xu%ZJB-lmdb`GD=*--C4h!i!FMOlGlZxQW!ElL=#x; zRX-=uYvy!h2)YX+U(hN}CGW)kT{`PY1-xhplpOvft{WwOG^_AR{HsLRh{9RN)COi0 zxqZkJyeD`;`(V(Yd2S~p5{jEQz{Yl9n@E3C?+o{6*bKT_FA@fHzXl&Mv$=kSeBS$G{%*`mmWz0u7~S@pxT#8xdkby!a#L;!oF4 zyJ$^ZfhoL zx$r-hA@j@Gmv@UPOtIZ)1wVy^Q}EaWjRN$J5+za=i|gRSm<5a@@H|6Z!aw znn3;c900=&{B{c7H#4<7TSp|pCQmyX zY)^(#OW%bpfSZ_M0jN7uCdnHTnBrINT|_&_!+ELc`DTEJI7R)UlRjPYkV}T;4T=aU z#@!r`oEfYUiL*2Ch^Q(UEzXKIGk1z^oww04L^IeQTrl%AxQ%;7LOSKUkB1iU1yidD z^nW;R-uhmOouxatt6+v(PT6P2m((Bg z6W`d3MEH5+yk(8*6x8mKFEmM0e%mCZO!YefZcs=*@MA$)N?T{U>*X733vBp>izVZ+ z;k|s#dRBA`X$n+)u5?XmC+ZaYQIDU4vb+0H-S|T+1vevS*7k9{H{DSs;Nj^7DcBMQ zyo759jX3&kQ05szF5^Uy7Np5i(2pY12LexGV%N|}rxF4eKi#O)#hdZBKK(Gq+rvt< zEtR|sKh_nbVD^zd4B&WxDTvN$$7IiF$7(+)NyCesI+@?NSBFIVJo_j-0X*;+5AL5z z;N23(>$yBkTg=ASV_BY=nEqYSC_>*6C1xzQ3c$e&{sl=y2*+X}c}e-ZOq}ZX-FukRin4^q_USa#mJXK!ex#w6kwCjJKpmj`b7|ccm2J> zx2^~_?p*+Z2%Tl+<#p<6+;-C%KUl6o_W3n6ok|u2FyHHDFVzz}e!R*?7~!i+w3y;d z9oV9B|1RsGtTMN&_%I za32NAo@kpt*OW9=P>QZGQlgzrA0xsDBhER_t{sVm@8E;Ku`Iw?@Qo~pi3hyV{sb)b z;D!Oe5nzK&+jv-46dOP(+*%+su}8qM`w`_a*B&sC!O4IcVI(|b=ghQ3K3eXZN$@06 zoUA@oeA0Y;Gyzl8|8n^Uxp;JrnHdAtT}9YUpL*mbhYx>pH>pe9GaylAX%d@Kjb_K; zA#8y^0$$Z~#|t{%+0G8goNeS2cVW2=^--1?dK<@eKs5ICZzJM&LCOd+pQd`T1-|7W zY1X@lyk+>GLg;m0xES2vAmp%5o!Ku6_a4HkemnXSJCV4ln7K5y5~`3)9L)dT`-2ki zd87G$a$^D+wYn#qEK{Yn1;h9VG#$%7beho4f5$(##T?-_m`KoXmfBR za}RW1QyF0UO;1s7^h1?d&w8{X)7n&;g*lvk0L<;hUsy+SrncZ+H z-(?t}c=?uNv^!2(A6VBMZlzDF%Heuy!^JJpRh=}dJCMh!^7B`I()?z9JV#X*5AgLh z4)D6tl8ilI#Y1*6USt&ty(}^4$++*9P+g$fma}y_d(auHuy-TVr>hrj@<6bI7-d3dLZi`$kzWR2#>-08VJE*lj0xmzKH$J;{&qqRuqj)@mkdiN#=A`|yzYZY#(|?D^ZV=GoI%I1CSNh&#m2nRm!N2cP*~ z0q7Ab|A6zC)I!5&VK<1D6~M8-ja+*0b?e|1%%lb`K_U+R@#i;ZaG>nNPwP_cvms1K z5ff1|24(U`OJt+G#6~VA|FudZgjt7X&PXn?E*(ACr#AaFUli6=1F1r+a!e0nzv_Ay zGBc_>#kV#Pe|4iQale=4lsUXL*`Oy`ivhI1-&&lPjtSe*i?2Y;NHzkAbZoD?J(&wO z9XJLOFXkrwq@SsTj{N&^w4_CMq~p@L64vLS(x=@&C(oxJ8BITvpMWV;l`m^_kATny z^yT%tL#TNjXr^5?SA5_8PIk_kIF$aDMe{f!l+oj+QOND|m*#}e#uhUe)sVXPA~v3} z0I#2X0*91z0F{vmTAq@gegR2DZtX@TM%>uP=R*%J29el!UgEym9Pk{EEVx&O3jVgc z%Hr{Q#82oUR&TS@4?UCamt5iX#2b)WNR@bJ#@JryddPu=$|!dxskGBUQ&Dd z%4Psn;td%}4|2r<6o5fj$?@Nzb{M?IPfP0fK+h zVIgnp8PLj1Aoyk&WrM@V3B}Ex-aJjrQQ>ZhCS)uFpNaW>N*MDCC#6TR`tdK8MOID` zX9@9cw6?eiBJvg8*fox+q0%3)dQ8#N*n8@&I9pSjUj4T|Kh-aLc+xOEE|h-EbpI;2 zsb0IzXXPV7-PajVxlzXR_kd0&E}b}ke%@krmeM;DfOWKGwwaA`{cHZET77^QiIzYw z`avM$hYy)<6)(Dn5<>{@vjG3__^O}4$EOwtdm>;=ehVRcOl!#MDkL16`fVEO(%2j1$s+j}^{Il!}Z)pP!$1K5Dv?4=a? z;AF3-I;!gD^ttusoMfUHO_h}KGGz+>-) z7ja|#(4U8J(U+C8sPL0^c->s##1}z@uIoRmvK(JMGy@`fe$SLE_YtI27D!)EJlY@FKJi=%Dr{XW=x4wWu7uHe`hs@vW_Ah_FzNqs%L^XgMb`fj+!)cEV~|jfDN> zR&EfW&)3XuJs_-=`$afI`_dHmI{yxPFetaJVqm_)4Wk$E(6#~CL;`%itbza-1@g`c zIELq^^W(#bK)qP&xxj}5$@q_tPUU(upz>ROAhnd4_eEzCp%D)?(LAa5DC41Ug*5Ct z+R(HUn?vUuMz%xo=vZ#cvH!I;c2YBL-kFEaKz)p1Vu&iMTD}v+ER&eXV7xuhgiE)Y zk}m#I3w&x2#aQ?3^+sHQD^xyfq@^3?=x$Kpq1gQTcamy111Xd@l6X4L>pvhfpIZ< zO465H*bZO*zG-?p&U4<-coD5K#K(z6A(v~BQ%5xnkURdu8+^cOlWgHA_>M=Nd%UjQ8_4-CaihkGmuEogO~{Ee zHqOV6Xi1H7wrYq_KA$nu>qB;gl$r!3P~&A(`oQV*g9|bUX-Pq>Bu5wuvqgT9!SAv7 z9sJ`~w~*n%M<$@a0T%$jmN^DSZUaLOXJGZSzo-c~AS8!tCSNMS*MA!#f~$heo3aw2 zAV}QJvb$wpfyeds!RtY^5sYITWz7MOvauqywz5p4_@OL z4Ob57L5m)+hcFkjQ(IdV20X@C&(=_g7~&C(CoELKI4{eN8A_Yk%Y|Mx*3{>Ap}Y{D znfV=!j?-z$czyWw@0Z-8f(qr*Gr2%Oq0TT;GOVi%y-f2NNx~jPy533w6k0> zQnwWvBGZ}Rnsl39txHg32k4ht?sZ}&vM{0=&&GVpkaPWx0}-?r<}1WU_qSPCh~**z z8Fqf6)+0X-`uN*ue6vX*e!v{f?Bj){sLa(@c*;3pR#~KGo7&$l9+pNnZ~yGK*IRTV zgEy(|-dC~fJ>lnoJRD2P8-itFm!wdvN@0PU?oNB~S!J5(s?@=7e*)!5{JA^^Egfq0 zB#v+f5}0X0sV=^3fQcIERCElFC7?9aQealM#s1h(SB;9sTbBDR%R*4y!vDeji^k4t zJafa|aUi~YgSFhT{V=3{2;0jtTe$0FdE)6baTnU6TwqDPsv(S+aeR4J4RGkfMj~iA z!an31qB}puODiRF;mc2kAhFJ;p>u>n8MoNPky_>qiCy@1(;+n&ZoI$(zR5TfecFoC z^1{G9@L8n57b%-FZt&U!_l{2;kOzSnod^|n`rp1w6t8g7!MD&3alpp{iOx$BL*}W2 zxiUgE^=~i^%9k}JOlq}=Q%>l~iKIS}ryXR`c(B2{CfDm{jOkWnseXj~r9=CGIOL>t z8>KmZjkL@eQ-(jMg#6L{sAl%`mfsnjc0cwImYu{ytMp~Z$Y|)`PcV{9yN;1sYF6{7 z&^VCVK6zU~r15U3X`WeKFq=c!*e)FhGW*8}WG<_Ri4PT0Kjh{E9q@>PY57!<-e5-w zb``WT+i5N;ea}0t!T(y&IF`8941SdV@Lq-;Se+}p7wE21ww6hC(c*PEaT(1;rdyIi ze6}evV?6LG-0oE@EU#D$MO%`3Nn_#Kt=rfQOESDgoIl#-MT{1>*FiV8ew63`30e=40x(MpZY6mDOc&25{_mN z6CpE$PMkgP5N0YdGs=O3sK_<1>aKmO?t>l?iFi+$Vx`{0SBNDdq-Wb82u~qU&crE^ zP}tl9?KRniodS>lQ&@UNz;DM6++m^mOinkermCwwzA=z}h!m>)2` zcMJ!L7zW^Y)U0Bz(y`KiWitJdU+KQNMH0xCSK&^uKzXt*wkKz`)cv3r5jd77Bk+P7 zKXfY^F?-s~hz1OMoNgwlOGPfgjj3Ax_Qr;&hrM8v9Ts=sImo^v@5)2g8)LXF>#Wb` z0*bMs3dPqj>q2LH4c=)>va4t{i{N}D?e#6Do76stD86dHi#=0MTQDY=yg&y89(UO) z1|WODf+G+04o_v1&nv+J&e{mdEFNaccBNVB3I56)v=}|M@cHavG4w0j{U|YERd?=l zOHEICv1r#uUjkm5qGS88L6NCZx1-Pz!|3|ie8m2tf;a>*4^QzU1@e3#|64OmUaNK; z)^Dc7kId!)cAh(-w5D0=pNj5l^V#hKisyS_PlNm<)R9~;dMvde3dJZ!6ukO8AVdgVR4>sl$7Q94r z;F`0y8(Qbe`$MQ>#*LdK_@H*V(>=lZ^)EmGW~TrEJ5-#88Kb9}H;0~F15k`gn8*X& z0833ysutU}8D~~SUmy!VkI79(zH8NUh=BX6Jl&hBXniapxiB7D;WuPL7q0c(W>L@R)ZgNCv_ zXk_j5aEu;8MCY;(p=u;uqf4zAI8 z-due3@a-73KlKC6TDt>6^VZ>1ictxf_-c13o>!Dd3c@~?l%nP-bjS(R1zX$vM%Bb9 zre%ir+uBUC9@kJTZs_MWT(NruA8HTnlD~E*JjS!!IUiEZ?p ztAftqpuG&hk3D$p$(I*{zvE!r4Zj26?l(Ms321*0=&u4K;^2!G*L93SMN(A(cs;#s zwAhNp+4!K~P|-8}TlN;xW}VQ;Lea$rAaom$v(f0pA3i)4Q7cNlR@^0ow6Zua)Z5lu z0&2g?YzPf{so$iS1i~tXjv~{l56qZTkPM$%;FQVI7#G5eBvOLwZ ziVk;(;z2y8(H$`}O!Q9viXl?Bmif9QCRd(MDf4-t<-746!#U9QEVIcc;dKvvn0Z9o zGy3}Cbe;}nEZD&;%UWPrq%!KgxJ=kc&tLn@1W(EZU#OqfCdkod4DBBS_$}A3-1SRU z4|fXO(HVfd-|+YgfOgG{KVT8|T0v5v!|UY;T~{)DxaLK#t((;+m)FX?3lddh~0 z?OUY#0vyiXn|UI3wk?^9qA^m50L$KKu#UNc7-Y z4_S0R5a>HE9vwyFXZ?)r7osUCowjM%hC$%707FrAF6C%wEl6cL=0o1#I+l359W{as zjC13p`Wyz~QTeL)6)0-uy`H?c=-&^%(HgNm?r8jJ9l0bT8-GX>y7qGhP0D!r>KJ-a z*7(`lSjdV@sGJzB63=gNnO!?cBpcP61(7#W?mStEUG<6Aq~TbU&u*^kCA(SMB)LmN zZACdVjOMS(gs}*;$b&^}8an!`dhgdBy7trywD&s+?pO!l?l(N%0K69HUmPBE6K4R> zSI$iAW*0jN+GteByqTq$Lz^wpZGkk9ui492K~z~dnzD~-x;&h$*S8$vn%szYZU z7q?Z9#zmD4{n`gT^OSBdCc)B%UN(P&Fi`QhDCr#sJIDEoa{hv?BI6(AgF6=Ir11Lq zECm14Hv?wh6cB7QH!AN@W*OYDJI&F7)7H^(=<%O-XWoPyb__b0<~cc@X7R1K2Nw zi1tMS?~AUVx$bCdop`{D7Pu<>l#&yL=mt%O+xVh~_V$WUSXpaECGe6B{Y+8yF4v@5 z$~Jr;k{uoRFHiH5J~mVn8eEa8!mg_i-e^5`2zYOjXcs(g3_T^wGRnLUDmNP3ipOLq zIFj%7jEqUSH3peq>vOh8HE|;R@#=)Cp*0#)Gz*sRMMk%cU6X%AGY_myu6u53PKdu5 zAaq+D&jK~VW3tTi=wsO$M-A0n>3x~?IWR8^y^ZLk>TP)zwrX5`*tsN=cFusUj0{G) z4LRpdSbqlIo55!%Mxe#H;BN;kec_LE^gliy{Ot{Qq~~+@8{YHfK>urqT7VX%6|rI% zf@wxfKa}B%ptCHBcN$z$D$@HhZ5rL#7K~Cx$A{oJ$hLrr{g0_A%;;OM)2o3k(gMI2 zoSh8_Ixq}*VmJiv`N7Ujbh?gqO1lnc1KjY^z=F4hAtP3ynCgNmzm79I=mf@mMS{7z zVE~iSKRFSePKa7@>prqM0|(CsFxD#ipUB7|2nX(b4gt+t=svh&nMmBM(>N$6-R33l zDJNR1?h|gA&yol!l~lhvMF7oRkpVdF<3Y+NDmU3@hhQx9)fu268itPL?`^DYEI=^F zAT>Hj8{Kop34^xr>Lfc?)+(IK=>Wd``jxwW@CfGIR&d8T0C&IP@y`bE698TUK&+rB z2XP4ea2d%{kUe@}2bfaDpf?JGA*jNAdKi{aQc2uo43j>l<2Hlv*?3RdShZ58B-0dw3AV8zJ?D(w@ZolvXhuC#bT9{{uyEFXqyg+n)+src6e zml85M{KseFz@h>?=d#{R6N^I^IM9}@qsC4~m36Rkty6iXieo~rN7b~}{2<;+q46u9 zqStu@ly8YA&3P&x@+?m9h(hLb8=@oa(v957I{ZlltcG?5;=C-V%+5-Lohxa@{FrcguNOOElUQU+NXPU z!udXwBbF78OB#NW5g5PAdO7@tz+@indFXJ^Qk{iJ&Hb^?-v;wkmfZtkJ-e-;^-)9n zAb`)le&w!r)|B3UaL1ehxcd!{-;dtk7K}fz2tg(IuDk&j7(3}|V%b**QkTa#h=cgl z?yX^LRd}$bMEbbr9z{t-TG&XA(x(6KG{!;n2Cy<*l#(xW_I1qDw+B6E0wVQ>-s(>L79rID?UA9j;2gF-ca_U=v?I`5UU zLspH2_|gWRKnde+!*Nry*)U|Lr3CB7*^U*n^3ghPYOf`&B}T+E&HvI=wL z1MyAYRN&WO*F9x7{{D=|MS>o2P~B4Gy3AjeXO8-32LbbTfUb{S{u+Hqyuv1Hizna~$>8qYa95o2uYe7&sq{l6_Um5pJ|XZJzLM@<$FOLf;}GK++32tib)RG1ne&O7r0a2~tiS0yhnoiS^pnVX{D zNMWpBjXRvWSA!?UBW#_7`C;J=p5LgLjJ6o2upCFancoz!XY2%cy;&-J=D3wC8*c;H zc$FtsK6424pk;QJsqA2oKn@9A4}C?Bh`VaR!nNE%M!X&9zm| zyVDxqiibM|N^=^E^IvK5M!5`Q+{_=nuMSP}YlJZqFjG*naFfH?LqdbgWL2&IH^*a< zy-salj(S)>qWF})&JUGWEKetS$^o?2ep^TX3l;X;4K}Rd?E!bc{_)=p;QN<5Thwqt z8-{0nK5-DHAJJHGRi}_u-OPkhFJr^C%^(*3M`)_ZpxQf}jHPjtKm;u~GYGB1SKtrc zhPBNk<+A|?Si$_Yx!ZE)98*UyG&BeCEs8iVhy28GqM30{eyjaRVF&cYT zpM@cr=}m6yYoHeXx%u6KN;*j?IBjYu)X+{8LTs@+XHk9y9T~cj!QdK|lX0YU zN}p`-w=iuR5y3wi9X+S=D%VO&@tIi+0_Sf8P^I8-ujOgI7vrMl!KfIm)c5p|AmiHd zFK0A>`kuh~UZE^83QzQK%J8gAPyfV*G+?#lpv5X+x|9KvM6(*Rh?Ynk^9E@+h@V;K|$)(1LH zm&Ptil^Kg033NwZa0zLnb&?fZ8sa+s@aj}V6=^LOJkcV!7uuEPPSu;&W$&N$o*BIm zJ(hHcD2M2re?b$+#G3-_C3-We@mZ(e!o5n-GCyv`IC}hlTX&R=u9crT8t&uAMYM-U zrR>b0isD)u|H7kUjvY;<;ak3^Er($6?YGD}EJMisnj#Me;b=iRO=_gnGEIYmEypsV zG(muxZMeSmIdck>hQNDr1tpr->qA+A!%)n*$%bS<%u#Ul)oBPeClnO;TC~Qi z{)+h~e0K+a zy_K24S=K!X4lCtkGun_^kbnny8qS2Z@eE%di_GrFa@7Mj_JD}8zRVJ`Q4I?%8hcU& zG;-+%Se4}XT_SNFTW*v3JDZHbX&**5yf3|ugWRj6_lE5{t9ccS!r362l@Wgkgy}1^ zZ0y6Ff@q5h8|b)P9<;BD`K9q$4MyO!?ZcaMv?@ZH*Je&?a^x7XIl7gSROet~2-bOJ zlf#Dudygi0F5ks{m@{E?6hR3e5j8 z{%GnVo-v}Lzg{>{MU=So%?e%1647gwF*h3g=0t=ksdXH|@jr`JhSCliyj@-6sFA~?cMh=D||>$>*;xzG=9ETN1&=F{loeP%llI1C-9{% zA9@QBw_S9WbEclxV~>xltr;RR=;`2J5R3V1u|8j{M}nhb*IR};D34Le&E2nm z_iyjO4+A*S*Gm9oWfP*o^;Zo^rGy5;)JIIGwd%)FNBJ{1#e`6e?J%ZEDX{Q!eaGC& zG~*-h?j&+e3PgSi>K~8IQ&QOP53D&<->Xl4Xt!rgORUGE)32JuMoI zxW`@(Jq$j`7mdqr3z=g+yUgeZgP1pZW@6ADfSeM43#8_5q}7KdPJouvuQ>+Qf!JAZ z!Mjr8tGU$X&ZBs}G~;n6<$lEQML(oDrfa{<2g^8vlMdo(7wdS#*9^h+i0_r2nCK7M z--dlASKkPjodqb*PpDi>*Pb=<3GG8`3B)ZLxS&RT0T&< z|I0=Pw;bG74#1_?zx$N{{vm(|0G2H_tRSON2!y7vHbJaeT6ja)!#lBw^A_UD1(z%Gx=KN-Wb(4)aI|o44fs+AC z*K9yW)$-K4+GBU<^fS1t(eS=ho_jHE6Qz9~X!E%XT@`IM%a5Ul49Di%0_r>ibiYnY zGW?M82|Z@OR^z1a{JYU-hploh>3MLz^I&8ER8Qz+V?X5!vqYV@+5K$(1`%)>2Xb9@ zo=9KTnyam1O7gCFrlO+ys-UlM;uh4wIrR^%M-b@`538_n2=43!TeJTpUjY{I-I(ki2r+mFFaNYtK#BqBa>Qp=?0not9 z73`Mg)DXL#T6w*;uhPWb=8LiSp&$cs%^!|#*zqu)&LVFDo&P4vJQki0&p6i&& z)2w`LqsCwP7WQ{XC0dve8MXDKyeSqU&*R!x;iPax zyA|DsgOhZv2LyG_BVmq_k&iK$l5wY=oF)m9yl)`SoA)r`VWOoMbGz2-e6N?G&zCl| zA9(QElP}TUZdJH#8~^~{-ht1w8{JXnxwkd1;?`MEgRtqfYF>sG?XbBT6zR5hB9W5f zh%O7c4^ddV^+#T8p>$aJ51M!YOJAl_*t_OTU2EEA{cVbG#urh%rVx@+T5#591hV1q z`81`n6+PNvLHBm3&p{XrP28shSXSa&0pq~n5Fb-K#2*d68`TpQ^p)0b%1cxQCDLTlgWYlBvv+Y~_- z@|dm@C$b@7{&q5|;%LNI-`Z-chEvmdZldzaeK|QgpNm}UVZf#8n8Q}gQw4Dij#ucW zG`VsadMT<%ayaN2Z}0K+lx{IQ{8Pg9M(DOVWc6SUSp$0SpM{QZ-|pnrfx|O>3&5q< zz3bIz?QK959tB=IteDKojr3bup+&0MOxb;Lh}lNHUV zZ1v$0NtMbrlC52c>Z)jQrZ|E_MaGw`r5gLdPg0OPwk8IKmBok(5fxfVXPmoXyyLTh zx+OGQKF5S9c|Ls}D`8L}9C$`a^?Vq-kWzN`ivd0VKxA0s3_w}dvf_@mw`CgULb)w> z)*nT%v)A3yKKntaltHm-tv_39>I7Ret4njIhX)sZaWckMlCsI&dL+dbz@d7BmMw!z zvPinmha|5kuo-T}Du`geak zdVdF6dl0Yz+5THhDXSpJ`Wwa&7y`Dvz6G)quyj3lC>qx(lw;Y48Hz9pzBFN_eLo3L zhq9S_H{Qf~zdmkN4pf*+#~#=zfN~G+a~10Ab!Y zQMW87-2O0a6JwjKJ5`~z?-?7_t!r5m893Kr&%&Y33@_*R6u_sSUcK#cT8I;4?mSw@yNo@$%R8|bS{K`$-^E81s+tx$%ZgqN0b9Y5Q zkgqsNSXpj89oI%R@2qFCE9JnIOiA`QJ~kqwj5)k2ppd?`|E=v=Ik@s0G?^ooRq^PP z-*AZ2L3h?qWl*LXHYoh>U)Ma$@D|Oc#DNy79-A@sCwkcZ107;rZyY z)3Z3wl@pX4EB?pjx|Y~U6s%#?8u?d-o}F1=SXL#^r>Fnf!9{H>qOp@#NPRxI4_W8+ zsD{BJ8u&W^!-+5^dhM){&s`E z0&9IbOT@WY9fzb4l)Nq#WL=LnJDf^VP5}CY03Lhr+LK3mK{qGd(hdN6|626^B}wtX z^j$BN&hjW4Shh=2ag+xflSPSGW$cZu20mJ-VOUq^!&C?hv9=k%D&>7Bs9-qc-W79Y z{Kf&vWCqx7QGxHhCmIF+(D0F`!9lTwKL+1RdRU1TjvX6}O379E`x11{O=)JABr4b3 zW+@X&T|P)*H+I~@sIooo%!}a=*xs=^3HfUa3NwK|l3&7CH5AG%Q|Mv1JZII`1Um+Z z#*%599iFsbHZXCUiHCFIEmx#EU_`^v#{AU{qS1BL(EZoWECkwA)WRBP2eKeln9boL$GZNn2!a8diC4!*N@dhRVJAzH~?t@;j4B7a!8&))%YKM6%I{F+X9L4FJ9r zz#pnRzBS;M^b>;2!~aNlFU}guSLJG9gg=x%{@ckW6@{x-#y0X5~eznP?db zk>qob%N>9uc~Gv6+^cCt6H=^A@Ssr^-Z?PmQ=4r8Fy0N|?SwAn#V zgX#CJ44c$RbLT`)=JZIYGbe2rU^C)D=7tf729wt}l?70(qUDiq`br>Y31arhz7rr$ zO6u$ITAXbMY|ZD)!QZ06zoZHp4-V``~x?J zdt%0KYlpd9|3@matPC58Uskfos3*gHpp2swmA6CSyc+oV)!5Wsu=!i^kUAk;)Q-Cl zz8!k+ydH|+E*)#Sp5U8(Dk#i8d_XW5qQ&i!%{iOM!K5;>E=9T?+cl&+&6zaXz-I9+Cez%n*VhfYT&IeoqTjwH^-nLr%_Ea1=p0hGLu(_60 zc}iW*y3jiJS78@|7QIUJFs;MLFaPWeLEL&F+mHv0tcZ%bBg612bo}}0kZ)p`yL>k( zTzcKRz5u|FZB}ae)Nz>^m}CX=i&f$=ve9IN+Gw>U{4Lf?QF}mMM*a}mWYAPo7grOZ zUVlT1GnQ{|n0p$0F}WghFHOD4xa%y z^q&0mbwQR1b^vP29Pov=H+)$~eYEs2=9mK&%^(nR;gLE4)pboH!|ZV2q3s>PipZBj zmlldq&&8PWAG&A8e``Rm0ke}frA=rYf#CZ9e8%b3iyt|1z|T9}Vp{<1FQa!c{u95@ z(z>Q>)-v}DXC5oaV9~W1wN70}kDC--LtZy+rB!4vuA8+FlSN9sx=^d2uh0G0g;8GJ zV`E`L&R#Jo^Z35)NTEfWoXxDUb?jZzH^Zq;9(=GM5fw}zg-tm&@uwylzR5Y zC&?1T`&gD!pIxr|iq?$#L{7sH#aNoW43uM3HqVtESD2yo4)g9$J+FpdRQ(B@cB!o2h7BznIrIe)b+{Y%Cdm-(^zBZp3neR+tG>onm<1q&|X^);^u^7U9;yMF1_xZUj($b*BZ~jCRhxK!K#o#)J+Rm z(@ag;^;Cu^oFMAX;q|)FH1Tap8XIK3KjKSVLSHCrC-9F2m61Wb9(O}QXmIXhH-4qa zml|O3KJlwe-@z9&%YZuqj&pn)z#Icr_ePuHKL5NivXz&C^wGPfK*wPw>rQ#DGER!C z63~i&X_>^vdKj+C7WiL|Q#5?@bR%xb2y8uP(D|FTsWNour1z)@HrPxtqAr^OD>Yo} zk4O?rMk^c7Iy(~2QE-MQWC~oggg?6OqoR{RR^I@a({>1kL)AuiI#)pz$^40$3Tl~n z0WLeZR>G(dCwF5lc7OIor&lig$k9xA9^sa704}}mU6+9VPXWAgl%9k1Dy>_2r6HA! za4(e&S%Cv294f3^%BM0GV}Xp{=orMe7b31~{wif-{82?((a9g(DI6Ba@G+G1Sa{?t z^#e+S5pR0xMi0hFiBZX=*)k9nglC6%HH#4PTOJcF;@87*npl2i_>oGi#appe(Rs4} znImBK<2a`U*dS_oYKDu}Sv>5Px5SQya~k|vYQ21JYtoE?V9JJ+%GKBfz}wb*O)*kwJ~hI9c0~7Yro{+<2N|7Pleyq6RYH0%#_I!>d!gxEksU>ioQgEIhoM!c zhAW%}7$QzLa5>RhE!kX4A4+L5{&qh%@`sDX`YN91!E_8SwHJm$avdjpo(Rc^RvCK zX*2+EhSom!^vZ?5KOOW<05^38;L_{f`J&$YBfZ0oKVb7>hviHmFJ*C(WWp-rA0C_c zKVU4O!b~-)!HUWj0(-Ber>o^Ablq>AT#r^-(rtYB#>6}zNPSvEt`j3)=4OMzlnklAbyQsyU^FtnZTRfi9D6lfFjv zI^pB%Ehpd0L&V$jq&W3-R8o!Z>WKO~>caQeINA9W{O^g&I~Cjw4r#$Yi5k6R1jeA z(+DkSsgzDgnH39zm5MhZ0qk-g2%}%E;vV){Fr?#`y670=@g*dSoNCyb80l{tpDtof z8BXRjtTjU>A8umBW*M0R9~+i{-i!;&$bq>ka*`_@q+p1?LxE`3#o(102xn|YuvTsC z5F|MWy{}LBtpS_azKWeV44n?fvQaJYh5?CesnK#LuJ7yw9|vAXnZD9#zn;NZGZ(6(dMqGEGUCq=3DCDA zsy74pEs8_05r&RKwu^KrJ4&px)<+Gw+%zJ;~To^yvV;X@cOU zfNl5xd4!AK_0CTL@K0K6FVgp?D}8tTP&)h_WkXU%c|&=q@2e+E12kE1L&64~-be(; zQRKZdK2^^BunBcVsor>*ZX|kSbW8m+VK6PWDc|G6^rWy=3!BAoC&vuRq6`uZC*|gN z<&YaaR+=5Br+#yJ=}4ADkNcb|9aQjrE#EYMmF((3G?H@T(uMY!Ohg$C``_GekSn3J z;8FQ{7~xy&(Bwlr2cDHiyiy=l-E%-ZSwGhNvOS=0=u|>1e6FTo@;eSsDvE+9W!E+@ zR5n}N&cAAoC%j=jSX|GaP2^D5Tv`Loo0#nI-14>!&y$aGmMuCOW^MY%fc6_tuU_~U zH-Pk6hnq43aPhm|dD0plX$@}t$C_;+*cj31h6kAzB3D@_d}GoBX7=!B^<0=&C2KS? z(GeVkhr*u4UK=(}H6DGO3C0<2SiAH1xn`H~z$@#^ddR zsDReS+V>0&z)`CjB<4a#<^YU`$ec1K>oAlq1-Q9p1o$nFnDJst$2i73wNM)^cqjodTAutg@|qIfP4t(&>YSxbIKSQz$bdvf!$w zz@vTo9V`-Ani>(6lI##Hq>ZQ~%6s?Um?aq0+VcdVOH%s5%K-|cal&W}>7Dd+-ASff zes&%pMg(Qv$a(9O-|6y^stO#vtM_rD%rU6=kBO`t)qDJfWrE0*Z9hU8A6J zJ$ZM#W#@pE#?o}TAJ+m~JYyQFd*ffgL;IwHYS4#(qZ(JB>)`w{cZ4<~;(WtzijEvf znMbYY%3Ud}TQL<5b|Oe3G(r1M4Z} zEoBE>ANt&8e*s9WGs=OLTY<)8QfTf<2PxF~mq7dE)2kP*pJ%wwHr#X>fZp+KZ2SZN z`%)I`uO3|KVd{j)Vruz%cz)#7^&aq=c~kLi^v`{8N^N;6jH5@2U^l@@VZ}WRjT+2g zV%BI$d1c1YbqP`LtH@T3RSIOP(=dKmIgjQ&>kuD54OCgdQDl|H;dX&P7;U^6@qm=8 zM+9RCA#&ikY>tzZB-6B+9hfPJ*gVU<{xX#-PKk1&BZT-YcTdmgw7*x0*YO})3p|eK z*?20AbMOOzeaw{`5SYR8$}s0LcFk*c)H}+{VxTr^=*PCi$}^h|US}0w2JuqJ^9clb zNq9^iTPP3Fzv!)|ZRnYy3}=_jNom@7vHs*b>l^CNvtIS~Kpe2>WqTHMwbsw1X<1l_ zO+&V^oj95(q8_h>X1`q?T?YEM;wFO|aKWBExcFVa@S6er10=ULuUjjlJ7l5M@&m&? zG7<>JReDh>%Gw$!G6plWTi)@?jQX%3yuVpc&lHV^)=ux`Iz6hmz⪻3&Wucb7acH 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%0D~s7CgyXQr|v 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

AqC#jf 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⪼XIvz$+JW(U#vKhu^cZ#))z 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

*t_I_{gDNKY4m2A?>DRyP#jr^-fSYrbeM?pJ6 z+Qh}2rsp!Spb4}m_haa;6q_|S%E&iTozqs4kUJm#c)<#OR}(33qF49R$tu&Fn;!4s z{th$H1`7FVng6~w@wl1%8t>v$_88Ee6`+DKWTWTYoSvbU6H1(bj*7>b^hxPrl66Md;)aJ=EgO0PoX-=4%TvOFNWBB{wTopvV-}fH+EcmSfgn z)~X);vG1+zHM-j`Z@tqN3nPA5`d)D+LgIM+DIzF;_ywncZn?#H9bP)F*%n%UZ}o)5 zEr1QWSV89zLWB5Lgn<8u>Tio1Vp;LdSAiCGB_Eszg3`VU6c6t+Tu)nnsKD(@P5cz3 zmbqK2W7j^%d|a)rN)OU z&cr3ClitR390-mPbv^tKYS_HHQLWv!AuJzfewrj@&TM}ypFYATa@^c8y^;G+Kmo#r z+^s3wo8XG)s)QQE0S`+TN*jf}{#<4@+)W)T>SkQ&I7rqr)`=zGfKo|YOgr|CF3JbJ z3P=A@+TPmiaLCBz6=Yf>_u}X~U2#RsDgIEnzicfF-`Tfnf_Ws`@`b6_dMW`qy;9rh zI!@dblLUrRCsr1SC`z)U&Biaj3-TZrUqWjOSQE=vH{e7pZ!W$8s({_$*Y74AdzX%n z-I;C-%7CIjq?y()1IFibJD*fp{Ib7we^HqUG;RZvyiEAQf~VH6Q&-;Gdb_^XNad|b zG$WrqPi`ISk_hA&kCh?5Sh3Xl^QE`@=;UVQqAQ>RxYUUB?9!r-6S2GzE^A|^$X;4> z44S8GTts+5S33!M{zAEC-kasB-u=;W{z5&Hr8ucDNV?mZPeLnmsqDvaYnNyk{@B

AG>X2= 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 K=d#Wzp$PygMrX+Y literal 0 HcmV?d00001 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/searchParticipant_light.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/searchParticipant_light.png new file mode 100644 index 0000000000000000000000000000000000000000..c44145fe16d0ca81823b50f526c4219786608604 GIT binary patch literal 60472 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 P&Hx0Su6{1-oD!M4+YDEKyY6WXgv}H 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

GDv_>`Li5y?}`aqt0peE3(4}%#~xqzvf-n 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
LmaG8u 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 Q3MK>z>% literal 0 HcmV?d00001 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/voiceCall.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/voiceCall.png new file mode 100644 index 0000000000000000000000000000000000000000..1738602c813361806cc07436a8f78251dc81808c GIT binary patch literal 148609 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
ug?);Km%7#SYD$cawfX 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 ICKqr13u+(uQ2+n{ literal 0 HcmV?d00001 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/voiceRecord_light.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/voiceRecord_light.png new file mode 100644 index 0000000000000000000000000000000000000000..90ae6a87a3c4edc04587f758015be624d79840f4 GIT binary patch literal 370254 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


O71bS>9V0{vsNOWW`s&wISFZuWLo*|T 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

Y~Xjx|%(9}xX93X59?xu1n_^&4)XWb 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 literal 0 HcmV?d00001 diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 0000000..02808f3 --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +Have private video calls and chat using your own server. diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/es-ES/full_description.txt b/fastlane/metadata/android/es-ES/full_description.txt new file mode 100644 index 0000000..0a83afd --- /dev/null +++ b/fastlane/metadata/android/es-ES/full_description.txt @@ -0,0 +1,25 @@ +Usa Nextcloud Talk para tener llamadas de audio y vídeo individuales o de grupo, crear o unirte a conferencias web y enviar mensajes de chat. Todas las comunicaciones están totalmente cifradas y mediadas por tu propio servidor, proporcionando el máximo grado posible de privacidad. + +Nextcloud Talk es fácil de usar y siempre será completamente gratuito. + +Nextcloud Talk soporta: +*Llamadas de audio/vídeo HD (H.265) +* Llamadas individuales y de grupo. +* Webinares y encuentros web públicos. +* Chat individual y de grupo. +* Una forma fácil de compartir la pantalla. +* Apps móviles para Android y iOS. +* Notificaciones push de llamadas y chats (solo si se instalal desde Google Play). +* Integración con los archivos y el groupware de Nextcloud. +* Totalmente en tus instalaciones, completamente código abierto. +* Llamadas cifradas de extremo a extremo. +* Escala hasta millones de usuarios. +* Gate SIP: llama por teléfono. + +La app Nextcloud Talk necesita un servidor de Nextcloud Talk para funcionar. Nextcloud es una plataforma autohospedada de sincronización de archivos y comunicación, diseñada para devolverte el control sobre tus datos. Funciona sobre un servidor de tu elección, sea en casa, en un proveedor de servidos o en tu empresa, y te da acceso a tus documentos, calendarios, contactos, correo electrónico y otros datos. Puedes compartir con otros incluso entre servidores Nextcloud diferentes y trabajar juntos en documentos. Nextcloud es totalmente código abierto, dándote la opción de extenderlo para tu propio uso, participar en su desarrollo o simplemente verificar que hace lo que prometemos. + +Millones de usuarios usan Nextcloud diariamente en negocios y hogares de todo el mundo. Los usuarios empresariales se apoyan en el soporte profesional de Nextcloud GmbH, asegurándose de tener una plataforma totalmente soportada, ypreparada para empresas de productividad y colaboración, totalmente bajo el control de su departamento informático. + +Descubre más en https://nextcloud.com/talk + +Encuentra Nextcloud en https://nextcloud.com diff --git a/fastlane/metadata/android/es-ES/short_description.txt b/fastlane/metadata/android/es-ES/short_description.txt new file mode 100644 index 0000000..cf574c3 --- /dev/null +++ b/fastlane/metadata/android/es-ES/short_description.txt @@ -0,0 +1 @@ +Haz llamadas de vídeo y conversaciones usando tu propio servidor. diff --git a/fastlane/metadata/android/es-ES/title.txt b/fastlane/metadata/android/es-ES/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/es-ES/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 0000000..0dc1e73 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,25 @@ +Utilisez Nextcloud Talk pour passer des appels audio ou vidéo en tête-à-tête ou en groupe, créer ou participer à des conférences Web et envoyer des messages instantanés. Toutes les communications sont entièrement chiffrées et transmises par votre propre serveur, offrant ainsi le plus haut degré de confidentialité possible. + +Nextcloud Talk est facile à utiliser et sera toujours entièrement gratuit ! + +Nextcloud Talk prend en charge : +* Appels audio/vidéo HD (H.265) +* Appels de groupe et en tête-à-tête +* Webinaires et réunions publiques sur le Web +* Chat individuel et de groupe +* Partage d'écran facile +* Applications mobiles pour Android et iOS +* Notifications de chat mobile (seulement si installé à partir de Google Play) +* Intégration dans Nextcloud Files et Nextcloud Groupware. +* Entièrement sur votre serveur, complètement open source. +* Appels chiffrés de bout en bout +* Adapté pour des millions d'utilisateurs +* Support du protocole SIP : composer le numéro avec votre téléphone + +L'application Nextcloud Talk nécessite un serveur Nextcloud Talk pour fonctionner. Nextcloud est une plateforme privée de synchronisation de fichiers et de communication, conçue pour vous permettre de reprendre le contrôle de vos données. Il fonctionne sur le serveur de votre choix, que ce soit à la maison, chez un fournisseur de services ou dans votre entreprise, et vous donne accès à vos documents, calendriers, contacts, courriels et autres données. Vous pouvez partager avec d'autres personnes, même sur différents serveurs Nextcloud et travailler ensemble sur des documents. Nextcloud est entièrement open source, vous donnant la possibilité de les étendre pour votre propre usage, de participer à leur développement ou simplement de vérifier qu'ils font ce que nous promettons. + +Des millions d'utilisateurs utilisent Nextcloud quotidiennement dans les entreprises et les foyers du monde entier. Les utilisateurs professionnels s'appuient sur le support professionnel de Nextcloud GmbH pour s'assurer qu'ils disposent d'une plate-forme de productivité et de collaboration entièrement prise en charge et prête pour l'entreprise, sous le contrôle total de leur département informatique. + +Pour en savoir plus, rendez-vous sur https://nextcloud.com/talk + +Retrouvez Nextcloud sur https://nextcloud.com diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 0000000..d82479c --- /dev/null +++ b/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Ayez des appels vidéos et des discussions privés en utilisant votre propre serve diff --git a/fastlane/metadata/android/fr-FR/title.txt b/fastlane/metadata/android/fr-FR/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/is-IS/short_description.txt b/fastlane/metadata/android/is-IS/short_description.txt new file mode 100644 index 0000000..e0c83f1 --- /dev/null +++ b/fastlane/metadata/android/is-IS/short_description.txt @@ -0,0 +1 @@ +Haltu úti eigin myndsamtölum og spjalli á þínum eigin þjóni. diff --git a/fastlane/metadata/android/is-IS/title.txt b/fastlane/metadata/android/is-IS/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/is-IS/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/it-IT/full_description.txt b/fastlane/metadata/android/it-IT/full_description.txt new file mode 100644 index 0000000..21a66b8 --- /dev/null +++ b/fastlane/metadata/android/it-IT/full_description.txt @@ -0,0 +1,25 @@ +Usa Nextcloud Talk per effettuare chiamate audio o video uno a uno o di gruppo, creare o partecipare a conferenze web e inviare messaggi di chat. Tutte le comunicazioni sono completamente cifrate e mediate dal tuo server, fornendo il più alto grado di riservatezza possibile. + +Nextcloud Talk è semplice da utilizzare e sarà sempre completamente gratuito e libero! + +Nextcloud Talk supporta: +* Chiamate audio/video HD (H.265) +* Chiamate di gruppo e uno a uno +* Webinar e incontri pubblici sul web +* Chat individuali e di gruppo +* Facile condivisione dello schermo +* Applicazione mobile per Android e iOS +* Chiamate da mobile e notifiche push delle chat +* Integrazione in Nextcloud File e Nextcloud Groupware +* Completamente on-premise, completamente open source +* Chiamate cifrate end-to-end +* Capacità di scalare fino a milioni di utenti +* Gateway SIP: chiamate tramite telefono + +L'applicazione Nextcloud Talk richiede un server Nextcloud Talk per funzionare. Nextcloud è una piattaforma privata e gestita in autonomia di sincronizzazione file e comunicazione, progettata per restituirti il controllo dei tuoi dati. Può essere eseguito su un server di tua scelta, sia esso a casa, presso un fornitore di servizi o nella tua azienda, che ti fornisce l'accesso ai tuoi documenti, calendari, contatti, messaggi di posta e altri dati. Puoi condividere con altri anche attraverso server Nextcloud differenti e lavorare insieme sui documenti. Nextcloud è completamente open source, offrendoti la possibilità di estenderlo per il tuo utilizzo, partecipare allo sviluppo o semplicemente verificare che faccia quanto previsto. + +Milioni di utenti utilizzano quotidianamente Nextcloud a lavoro o a casa in tutto il mondo. Gli utenti aziendali fanno affidamento sul supporto professionale di Nextcloud GmbH, assicurandosi di avere una piattaforma completamente supportata e pronta per l'ambito aziendale per la produttività e la collaborazione, completamente sotto il controllo del reparto IT. + +Scopri altro su https://nextcloud.com/talk + +Trovi Nextcloud su https://nextcloud.com diff --git a/fastlane/metadata/android/it-IT/short_description.txt b/fastlane/metadata/android/it-IT/short_description.txt new file mode 100644 index 0000000..9dca715 --- /dev/null +++ b/fastlane/metadata/android/it-IT/short_description.txt @@ -0,0 +1 @@ +Videochiamate e chat utilizzando il tuo server. diff --git a/fastlane/metadata/android/it-IT/title.txt b/fastlane/metadata/android/it-IT/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/it-IT/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/nl-NL/full_description.txt b/fastlane/metadata/android/nl-NL/full_description.txt new file mode 100644 index 0000000..759bca2 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/full_description.txt @@ -0,0 +1,25 @@ +Gebruik Nextcloud Talk om een één-op-één of groeps- audio- of videogesprek te houden, voor het starten of deelnemen aan web conferenties en voor het versturen van berichten. Alle communicatie is volledig versleuteld en verwerkt via je eigen server, waardoor je de hoogst mogelijke graad van privacy bereikt. + +Nextcloud Talk is gemakkelijk te gebruiken en is altijd helemaal gratis! + +Nextcloud Talk ondersteunt: +* HD (H.265) audio/video gesprekken +* Groeps en één-op-één gesprekken +* Webinars & openbare web bijeenkomsten +* Individuele en groepschat +* Eenvoudig delen van je scherm +* Mobiele apps voor Android en iOS +* Mobiele gespreks- & chat push meldingen +* Integratie in Nextcloud Bestanden en Nextcloud Groupware +* Volledig on-premise, 100% open source +* Begin-tot-eind versleutelde gesprekken +* Schaalbaar tot miljoenen gebruikers +* SIP gate: inbellen per telefoon + +De Nextcloud Talk app vereist een Nextcloud Talk server. Nextcloud is een besloten, self-hosted bestandssynchronisatie- en communicatieplatform, ontwikkeld om jou weer in control over jouw gegevens te laten zijn. Het draait op een server van jouw keuze, thuis, bij een service provider of in je bedrijf en geeft je toegang tot je documenten, agenda's, contactpersonen, e-mail en andere gegevens. Je kunt delen met anderen, ook over verschillende Nextcloud servers en samenwerken aan documenten. Nextcloud is volledig open source, waardoor je de mogelijkheid hebt om het uit te breiden voor je eigen gebruik, om mee te doen aan de ontwikkeling ervan, of om simpelweg te controleren of het doet wat we beloven. + +Miljoenen mensen gebruiken Nextcloud dagelijks voor hun werk of thuis. Professionele gebruikers vertrouwen op de professionele ondersteuning van Nextcloud GmbH, waarbij ze een volledig ondersteunde, enterprise-ready platform voor productiviteit en samenwerking hebben, geheel onder controle van hun IT afdeling. + +Meer informatie op https://nextcloud.com/talk + +Bezoek Nextcloud op https://nextcloud.com diff --git a/fastlane/metadata/android/nl-NL/short_description.txt b/fastlane/metadata/android/nl-NL/short_description.txt new file mode 100644 index 0000000..2d68425 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/short_description.txt @@ -0,0 +1 @@ +Heb vertrouwelijke videogesprekken en chats via je eigen server. diff --git a/fastlane/metadata/android/nl-NL/title.txt b/fastlane/metadata/android/nl-NL/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/pl-PL/short_description.txt b/fastlane/metadata/android/pl-PL/short_description.txt new file mode 100644 index 0000000..0f9b012 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/short_description.txt @@ -0,0 +1 @@ +Prowadź prywatne rozmowy wideo i czat za pomocą własnego serwera. diff --git a/fastlane/metadata/android/pl-PL/title.txt b/fastlane/metadata/android/pl-PL/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt new file mode 100644 index 0000000..164f54f --- /dev/null +++ b/fastlane/metadata/android/pt-BR/full_description.txt @@ -0,0 +1,25 @@ +Use o Nextcloud Talk para realizar chamadas individuais ou em grupo de áudio ou vídeo, criar ou participar de conferências online e enviar mensagens de bate-papo. Toda a comunicação é totalmente criptografada e mediada por seu próprio servidor, proporcionando o mais alto grau de privacidade possível. + +O Nextcloud Talk é fácil de usar e será sempre grátis! + +O Nextcloud Talk suporta: +* Chamadas de áudio/vídeo HD (H.265) +* Chamadas individuais ou em grupo +* Webinários & públicos na web +* Bate-papo individual e em grupo +* Compartilhamento fácil de tela +* Aplicativos para Android e iOS +* Chamadas móveis & notificações push de bate-papo (se instalado a partir do Google Play) +* Integração com Nextcloud Files e Nextcloud Groupware +* Completamente de código aberto e auto-hospedado +* Chamadas Criptografadas de ponta-a-ponta +* Suporte para milhões de usuários +* SIP gate: discar no telefone + +O aplicativo Nextcloud Talk requer um servidor Nextcloud Talk para funcionar. Nextcloud é uma plataforma privada de sincronização e comunicação de arquivos auto-hospedada, projetada para colocar você de volta no controle de seus dados. Ele é executado em um servidor de sua escolha, seja em casa, em um provedor de serviços ou em sua empresa, e fornece acesso a documentos, calendários, contatos, e-mail e outros dados. Você pode compartilhar com outras pessoas, mesmo em diferentes servidores Nextcloud e trabalharem juntos em documentos. O Nextcloud é totalmente open source, dando a você a opção de estendê-los para seu próprio uso, participar de seu desenvolvimento ou simplesmente verificar se ele cumpre o que promete. + +Milhões de usuários usam o Nextcloud diariamente em empresas e residências no mundo todo. Os usuários corporativos contam com o suporte profissional da Nextcloud GmbH, garantindo que eles tenham uma plataforma pronta total produtividade e colaboração, totalmente sob controle do departamento de TI. + +Saiba mais em https://nextcloud.com/talk + +Encontre o Nextcloud em https://nextcloud.com diff --git a/fastlane/metadata/android/pt-BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt new file mode 100644 index 0000000..41ebbc4 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/short_description.txt @@ -0,0 +1 @@ +Tenha um chat com chamadas de vídeo privadas usando seu próprio servidor. diff --git a/fastlane/metadata/android/pt-BR/title.txt b/fastlane/metadata/android/pt-BR/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt new file mode 100644 index 0000000..3ceab52 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1,25 @@ +Приложение Nexcloud Talk предназначено для осуществления вызовов между двумя или несколькими абонентами, в том числе с использованием видеосвязи. Соединения обслуживаются вашим собственным сервером, и все данные передаются в зашифрованном виде, что обеспечивает максимальную защиту. + +Навсегда бесплатное приложение Nexcloud Talk простое в использовании. + +Возможности приложения: +* для вызовов и видео-вызовов используется кодек с высоким разрешением (H.265) +* вызовы между двумя или несколькими абонентами +* вебинары +* личные чаты и чаты групп +* простой показ экрана другому пользователю +* приложения для мобильных платформ Android и iOS +* вызовы с использованием мобильных устройств и push-уведомления (только при установке из Google Play) +* поддержка приложения обмена файлами Nextcloud и приложений для рабочих групп +* размещение сервера на вашем собственном оборудовании, полностью открытый исходный код +* сквозное шифрование вызовов +* возможность масштабирования до миллионов пользователей +* SIP-шлюз: использование обычного телефона для совершения вызова + +Приложению Nextcloud Talk для работы требуется сервер Nextcloud. Nexcloud — личная, размещаемая на собственном оборудовании платформа для организации синхронизации данных и взаимодействия пользователей. Nexcloud возвращает вам контроль над своими данными. Место размещения оборудования, на котором работает Nextcloud, так же выбираете вы: у себя дома, у провайдера или на предприятии, Nextcloud обеспечивает доступ к вашим документам, календарям, контактам, электронной почте и другим данным. Обмен файлами и совместная работа с документами возможны даже между пользователями разных серверов Nextcloud. Исходный код Nextcloud открыт, что предоставляет возможность изменять его для своих нужд, участвовать в разработке или просто убедиться в том, что он делает именно то, что обещано. + +Nextcloud ежедневно используется как для работы, так и для личных целей миллионами пользователей по всему миру. Бизнес-пользователи уверенно полагаются на профессиональную поддержку Nextcloud GmbH выбранной полностью поддерживаемой корпоративной платформы для совместной работы, которая полностью контролируется собственным IT-департаментом. + +Узнайте больше на https://nextcloud.com/talk + +Познакомьтесь с Nextcloud на https://nextcloud.com diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt new file mode 100644 index 0000000..c01c62a --- /dev/null +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -0,0 +1 @@ +Личные видеовызовы и переписка с использованием собственного сервера. diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/sr-SR/full_description.txt b/fastlane/metadata/android/sr-SR/full_description.txt new file mode 100644 index 0000000..235ed08 --- /dev/null +++ b/fastlane/metadata/android/sr-SR/full_description.txt @@ -0,0 +1,26 @@ +Користите Nextcloud Talk да обављате приватне +или групне аудио и видео позиве, правите и придружујете се веб конференцијским позивима и ћаскањима. Целокупна комуникација је шифрована и спроводи се на Вашем серверу, дајући Вам тако највиши могући ниво приватности. + +Nextcloud Talk је једноставан за коришћење и увек ће бити потпуно слободан! + +Nextcloud Talk подржава: +* HD (H.265) аудио/видео позиве +* Групне и приватне позиве +* Вебинаре & јавне веб састанке +* Приватно и групно ћаскање +* Једноставно дељење екрана +* Мобилне апликације за Андроид и iOS +* Позиве са мобилног & брза обавештења за разговоре на мобилном +* Интеграција са Nextcloud фајловима и Nextcloud Groupware апликацијом +* Комплетно смештен код Вас, комплетно отвореног кода +* Шифровани разговори са краја на крај +* Скалирање на милионе корисника +* SIP гејт: звање телефоном + +Nextcloud Talk апликација захтева да имате Некстклауд сервер да би радила. Некстклауд је платформа за приватну синхронизацију фајлова и комуникацију коју можете да хостујете где желите, дизајнирана тако да само Ви будете у контроли Ваших података. Ради на серверу који Ви одаберете, било то код куће, неком провајдеру или Вашој фирми и даје Вам приступ документима, календару, контактима, е-пошти и осталим подацима. Све ове ствари можете делити са осталима, чак и на друге Некстклауд сервере и сарађивати заједно на документима. Некстклауд је комплетно отвореног кода, дајући Вам могућност да га проширите за употребу како Ви желите, да учествујете у његовом развоју или просто да проверите и да се уверите да све ради како је и обећано. + +Милиони корисника око света дневно користе Некстклауд на послу и у кући. Пословни корисници се ослањају на професионалну подршку фирме Nextcloud GmbH, која се стара да имају комплетно подржану платформу, спремну за предузетништво, продуктивност и сарадњу, под потпуном контролом њиховог IT одељења. + +Сазнајте више на https://nextcloud.com/talk + +Пронађите Некстклауд на https://nextcloud.com diff --git a/fastlane/metadata/android/sr-SR/short_description.txt b/fastlane/metadata/android/sr-SR/short_description.txt new file mode 100644 index 0000000..f0beab5 --- /dev/null +++ b/fastlane/metadata/android/sr-SR/short_description.txt @@ -0,0 +1 @@ +Обављајте приватне видео позиве и ћаскајте користећи Ваш сервер. diff --git a/fastlane/metadata/android/sr-SR/title.txt b/fastlane/metadata/android/sr-SR/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/sr-SR/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/tr-TR/full_description.txt b/fastlane/metadata/android/tr-TR/full_description.txt new file mode 100644 index 0000000..d11a08c --- /dev/null +++ b/fastlane/metadata/android/tr-TR/full_description.txt @@ -0,0 +1,27 @@ +Birebir ya da grup ile sesli ya da görüntülü görüşmeler yapmak, web konferansları oluşturmak ve sohbet iletileri göndermek için Nextcloud Talk uygulamasını kullanabilirsiniz. Tüm bilgiler tamamen şifrelenmiş olarak sizin sunucunuz üzerinden iletilerek olabilecek en yüksek gizlilik düzeyi sağlanır. + +Nextcloud Talk uygulamasının kullanımı kolaydır ve herhangi bir ücret ödemeniz gerekmez! + +Nextcloud Talk şu özellikleri destekler: +* HD (H.265) sesli ve görüntü görüşmeler +* Grup ve birebir görüşmeler +* Webinar & ve herkese açık web toplantıları +* Bireysel ve grup sohbetleri +* Kolay ekran paylaşımı +* Android ve iOS uygulamaları +* Mobil görüşme & ve anında sohbet bildirimleri +* Nextcloud Files ve Nextcloud Groupware ile bütünleşme +* Kendi veri merkezinizde, tamamen açık kaynaklı +* Uçtan uca şifrelenmiş çağrılar +* Milyonlarca kullanıcıya hazır +* SIP geçidi üzerinden standart telefon ile görüşme + +Nextcloud Talk uygulamasının kullanılabilmesi için Nextcloud Talk sunucusu gereklidir. Nextcloud verilerinizin denetiminin yeniden size veren, size özel ve kendi sunucularınızda barındırabileceğiniz bir dosya eşitleme ve iletişim platformudur. Evinizde, hizmet sağlayıcınızda ya da kurumunuzda bulunan bir sunucu üzerine kurarak, belgelerinize, takvimlerinize, kişilerinize, e-postalarınıza ve diğer verilerinize erişmenizi sağlar. Verilerinizi farklı Nextcloud sunucuları kullanan kişiler ile paylaşarak dosyalar üzerinde birlikte çalışabilirsiniz. Nextcloud tamamen açık kaynaklı olduğundan özelliklerini gereksinimlerinize göre genişletebilir, geliştirilmesine katkıda bulunabilir ya da söylediklerimizin doğru olup olmadığını görebilirsiniz. + + +Dünya üzerinde milyonlarca kullanıcı ev ya da iş yerlerinde günlük işlemleri için Nextcloud kullanıyor. Kurumsal kullanıcılar Nextcloud GmbH tarafından sunulan destek hizmetleri ile tamamen kendi BT bölümlerinin denetimindeki kurumsal üretim ve iş birliği platformu için eksiksiz destek alıyor. + + +Ayrıntılı bilgi almak için https://nextcloud.com/talk adresine bakabilirsiniz. + +Nextcloud web sitesi https://nextcloud.com diff --git a/fastlane/metadata/android/tr-TR/short_description.txt b/fastlane/metadata/android/tr-TR/short_description.txt new file mode 100644 index 0000000..56f902b --- /dev/null +++ b/fastlane/metadata/android/tr-TR/short_description.txt @@ -0,0 +1 @@ +Kendi sunucunuz üzerinden görüntülü görüşme ve sohbet yapın. diff --git a/fastlane/metadata/android/tr-TR/title.txt b/fastlane/metadata/android/tr-TR/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/fastlane/metadata/android/vi-VI/short_description.txt b/fastlane/metadata/android/vi-VI/short_description.txt new file mode 100644 index 0000000..3c4db88 --- /dev/null +++ b/fastlane/metadata/android/vi-VI/short_description.txt @@ -0,0 +1 @@ +Có cuộc gọi và trò chuyện video riêng tư bằng máy chủ của riêng bạn. diff --git a/fastlane/metadata/android/vi-VI/title.txt b/fastlane/metadata/android/vi-VI/title.txt new file mode 100644 index 0000000..803a202 --- /dev/null +++ b/fastlane/metadata/android/vi-VI/title.txt @@ -0,0 +1 @@ +Nextcloud Talk diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3a53106 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,35 @@ +# +# Nextcloud Talk - Android Client +# +# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2021 Tim Krger +# SPDX-FileCopyrightText: 2017-2019 Mario Danic +# SPDX-License-Identifier: GPL-3.0-or-later + +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-XX:MaxHeapSize\=4096m -Xmx4096m +org.gradle.dependency.verification.console=verbose + +android.useAndroidX=true +android.enableJetifier=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false + +org.gradle.daemon=true +org.gradle.configureondemand=true +org.gradle.caching=true +org.gradle.parallel=true + +NC_TEST_SERVER_BASEURL=http://server +NC_TEST_SERVER_USERNAME=user1 +NC_TEST_SERVER_PASSWORD=user1 diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys new file mode 100644 index 0000000..ce65ac9 --- /dev/null +++ b/gradle/verification-keyring.keys @@ -0,0 +1,6933 @@ +pub 80C08B1C29100955 +uid Jake Wharton + +sub CF771F914C2A4A73 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBE2fCWARBAC3v9wYo5kmynmVP+43ccamidflSLQjjpsXpSDLPFokGxeuw0OC +QJy46m8b5ACoCqRlfwnRRcEHxiSlaBATJA6hi7NRO41R39C62JXsIxNJR16JNQ5k +oG/NOAraw0E1RQIFslznQexfxPg4yFIVrsFp1wkpCRrCklatPMNap2DuNwCg7PWJ +1vV93YIsaH0O2fnXz3E+6zsD/3cTzUWuySEDiCLNO3JYJm97v4NDQ93encP1Ooxh +n+PSIP4GvjrAObh3FfWUucv8UGqcw5bAL7dA1z8SgKeyFk/afs2XofXdvC+PhZqC +DwU0NiE0D/tDWqX0qIG4ezTU2uk+5dE/WVl3R10nOBgquQdWIdYKGfV4FNTiEduD +Uw7fA/0XcwFom7eyR9eBonQmgIadljztm4gkv11lY33V1ZfJNndPKNzwevDwX+om +/VEHvpEfPx5toD4H523BPx55ZtfowuMtFHZI718alpCo3h6xaDhGwXvsg3s9k03k +rfxzCjf9qcJX1gb2JVZ2+2jCwUDQZeEwV2vivjGNiN9rShWW+7QkSmFrZSBXaGFy +dG9uIDxqYWtld2hhcnRvbkBnbWFpbC5jb20+uQINBE2fCWAQCAC3lOHYBShJ/G9x +NS1E9ubL71FF9pLlS0OU86JAus41kKz3oTpbGMfnsvEUjf+gOawS7Z9c++a6Kxd/ +rzeZCzwM/Mdk4egsXepb59w0B37wy9GB5adAGc4R8eaIMicXWqsMzctCVsWr2hRj +em/G260dTX9PJKv3eXTXvjRlXQV3LqlKOo7dVvrZG57ZJUuXvgff0P+C/HWJJfMV +0SDALisOofI+CxV7HusZ3VnlrUyJH2SJs9H+3C0FeV+0Er/dItSn80/YS4lI6cKj +8nbUzbgevoHWuuIxTYVXL4FvoezB+hU/80rLNiYdng0eBxa7+xCUkvHSgrz6McvB +JfKUDBv/AAMFCACF6vq8sOOL6B/yHW700LSuLef5afzo/W+0KzEd7v4kMTOCt3au +61GwPIY3TDN4HzLtdweKcxraXH9uW+DSCEJIREvfglA0A2BxfoMJK8BGrHG3X2GY +iwB7XDI1tToPiIU2PMA7iU+CNaarpks1lfFMVSXLajVE2NnlO9efKA+fqa3QnHkC +288aChbbtOepGdvuOr+mwJfIP8PFqFTkVLh67rryqITFTx8DUMLOyhwhcvWAq+Mi +EH6gvIPKCE6pYFGXGWl8eivjZ3cAOiwKFyS9HnWmTmQbhauR+xZK8NO/jiEBai0x +nBaUddgMw4fZZrciUPJ1X2+iIwDksAT5RGVkiEkEGBECAAkFAk2fCWACGwwACgkQ +gMCLHCkQCVXefACdGAPMb9FOTnZvLttQb8sYxkt7QWQAoIBej+io4VD6SAGE0ur9 +07c4ZCXa +=7AnO +-----END PGP PUBLIC KEY BLOCK----- + +pub 81C27DE945332233 +sub 126BBD1EF340F4BD +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQMuBEvACI4RCACNKnrdBk54/vBJSu2RJCG3VXR1h5DPGxJR1QIXDylZC8g4XQoj +0cE7Ij61hd1VYAWlzlrfXtHi80BIXoGza61OaA0pUCxTuLyAL22x0GmdNpGBkigz +r58LnwaqTxmGwQk/1mjJID2lDzi+GeEtKZd2xmYg7thIYAaKTQaQAzShfsntIdtl +8XH7X7qpze+CGqh1cQR89aJXjdaspj9hT4Bgj5EfQk3DKUXDXAD4lg5IP2mLGpeb +MzBPdhdvF1L6nR+CbNgwCFdDwcqsYxaAc2gxFUAoX4U9p/rG4lfZ8ft3zkKv/N0c +2bRTpMR3HCSk/Q6io6C8JfEynpTXE1HjG633AQCpTien60HqyCD5o8riRDOeSN8F +Ib7CSH5l9A+eVcPWGQf/UuYeEjOPMl9RChB6ZpqmjwnXpDWlE8hVaWVOHp80CDuB +gxEcsWJ62VCiXJPrC7fBrQo6rjaSioP8rT2Ge8CSESANVBFLtuyhpNFeek0SJGQ9 +0bCplP9cizGDCVKxiEH8Toi1ifzeFRQYjum22YElrhpcPQhf0PLIxUmllFHr1P+t +f8jlfkf23IgdhAhUedVloh0oPS0epixPB098acZCRKPoYZzTXmEXjp5q+JGPE/Fh +3Rlt0SGXEcrHILQThpGjn5xG9xzH+WNYz6kZ01CLpgUdNvB291C0lvsVq3r4U0lq +eCZCNJ4JfCm3KYzAt603nlWILnmTynavaWDO2JYhXQf+OyuDPrOrYdkELczLKI4p ++1pEaVLQ8dZRL0W/XJ+RFMtmWyoQ4ypqlYkhxj+zslaG9UAOBwodIACMvlGrVHMi +BaY0q+/9oZtMqEKN+jp8z/sYyDlOPFeDOU00vk7XU0v6Vy4Jm9ogSitlMRuvSQxr +XGh1tDLbz+3MNyFjISdHxtc2gLqA74uAfIfI0Wwkx07xLwiI4y/GR6BB4oHVmytn +R2jjTquLfbNXRwf/4x1GVj/V7ztszVqNa3n4LtDcK0cfXFfwJ7/skhuGmReyzxjA +yTiSlbCf13zA6GLr1qZaJv3t8iSNA/TfP9NplbZoaROLhHSOp++GwplpbJFaBec2 +XrkCDQRLwAiOEAgA/CUGWGQsNucNS2dDPbk/lUx6MaFqRV+CcwAsCLUPCBQ22Crz +T1OvwnybE+/wc9KxqBsGcQxg8STGP01nFEttn755O55Avk19kWP5EWVzndIqsjFb +cInQHVAeglz6F0a/7SFmaznrKvWCeHwjawEbAQXkCd8ZPRqeP2RWpwcxjoxFJRhQ +nVaEJ2u8XAU5RCrfsnfaRD/99NcCrE+5xfyWncF6n5FEmgVt9GQNyjYZPzO3Ikzj +mpay0nboxpocDV3iTJe1aRBAZfUxP9R4mFMb0R7/6CP4YGKF4CnKCzUZFaD7aswh +mi+VVVFOcyurT+VJrX7Iaxe/305JWqKy0G/W0wADBgf/TZE2OQJ2EC+Dqkk74I1e +UPcVwsdDdzhVsRvjeyISg03nuTT/VZ/9qObCe69l1M4SfJNoqLm2QD8dUzWTBG7l +SseL4HjMckapIql8Vy5lNhnnzOjk2znIvaL4CuS0vFSGaXC0FS52BhtbxHmUsHr7 +xgASj95Us7dp3C9oO/iD+k1rWpoD7QungfXP7HrXxzoFiYFZhcY+YMdM2K/pr+Ba +z3vakSsNGU1vs96l94QWGUf681xGlyw2uyWRuNPWhMYgGSJ05LIAtm3vIcWfNp7r +T14lVSSJCqEP8/f/50N5MeqBXEDrOO4lj1meUL5MK8Nxk6qEacYaMwxTpz5losxt +7IhhBBgRCAAJBQJLwAiOAhsMAAoJEIHCfelFMyIzCE4A/iu1wm8vFwBxrqYWOO/c +2BbjtX4tROpH2+P2Fk13iYkMAP4xCfyw10teMvj6fu3u9+H1XvC1AQZTqHnaG/8Y +ss9xVg== +=Zs5g +-----END PGP PUBLIC KEY BLOCK----- + +pub 84E913A8E3A748C0 +uid The Legion of the Bouncy Castle Inc. (Maven Repository Artifact Signer) + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGR/8HUBDADJ+V5VgTXFG4xVI/1r07a/pTXoAQhHyJMkVdFScGARsps07VXI +IsYgPsifOFU55E7uRMZPTLAx5F1uxoZAWGtXIz0d4ISKhobFquH8jZe7TnsJBJNV +eo3u7G54iSfLifiJ4q17NvaESBNSirPaAPfEni93+gQvdn3zVnDPfO+mhO00l/fE +5GnqHt/Q2z2WKVQt3Vg0R66phe2XaFnycY/d+an73FiXqhuhm4sXlcA++gfSt1H1 +K7+ApqJsX9yw79A1FlGTPOeimqZqE75+OyQ9Kz0XTvN/GmHeEygTrNEnMDTr1BWz +P0/ut0UXmktJtJXgLi5wUCncwwi+UpCSwwou7/3r+eBh5aykxSo9OtYe4xPNKWSo +EiPZXpCH5Wjq9TpXOuhnZvRFqbR24mWz5+J/DoaVP3pwEhGXxr5VjVc1f8gJ8A34 +YYPlxUGcl8f3kykzvl4X5HDIbHb9MAl+9qtwQo1tFA9umD2Da/8bSsxrnZdkkzEA +OpJYwT1EkQRZRcUAEQEAAbRmVGhlIExlZ2lvbiBvZiB0aGUgQm91bmN5IENhc3Rs +ZSBJbmMuIChNYXZlbiBSZXBvc2l0b3J5IEFydGlmYWN0IFNpZ25lcikgPGJjbWF2 +ZW5zeW5jQGJvdW5jeWNhc3RsZS5vcmc+ +=/HDf +-----END PGP PUBLIC KEY BLOCK----- + +pub 858FC4C4F43856A3 +uid J. Daniel Kulp + +sub 594E23256A36A392 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +I8dIB8ab72h8ySjaeEd9kV96ByEtpm09 +=gqfS +-----END PGP PUBLIC KEY BLOCK----- + +pub 85911F425EC61B51 +uid Marc Philipp + +sub 8B2A34A7D4A9B8B3 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +PXCe8La72ukmaxEGbpJ8mdzS2PJko7mm +=Xe8l +-----END PGP PUBLIC KEY BLOCK----- + +pub 8671A8DF71296252 +uid Jesse Wilson + +sub 51F5B36C761AA122 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +RdJ+iwaDdWhzYdcKIgRuZ2bcN3HPa1XQ +=qSXW +-----END PGP PUBLIC KEY BLOCK----- + +pub 86FDC7E2A11262CB +uid Gary David Gregory (Code signing key) + +sub 59BA7BFEAD3D7F94 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE2kzuwBCACYV+G9yxNkSjAKSji0B5ipMGM74JAL1Ogtcu+993pLHHYsdXri +WWXi37x9PLjeHxw63mN26SFyrbMJ4A8erLB03PDjw0DEzAwiu9P2vSvL/RFxGBbk +cM0BTNXNR1rk8DpIzvXtejp8IHtD1qcDLTlJ8D0W3USebShDPo6NmMxTNuH0u99B +WHCMAdSa34wsg0ZpffwQmRxeA+ebrf2ydKupGkeZsKjkLlaXNkTVp1ghn5ts/lvg +KeHv1SJivWKCRmFlbPhBK4+mxSUSOPdoBNAfxA51QzZoPizSk0VbRz3YufYRVLFy +9vqPSorDmYJhCvn3f6+A38FS/j8VE+8obQ2rABEBAAG0O0dhcnkgRGF2aWQgR3Jl +Z29yeSAoQ29kZSBzaWduaW5nIGtleSkgPGdncmVnb3J5QGFwYWNoZS5vcmc+uQEN +BE2kzuwBCACzeGpkd6X/xTfKDBWvXgHOOKIJ2pht9XmtZZKiIj7LIiSwvSds/Zko +ZKxAm7AY+KPh8Xjf968FtoUBQJvHAG4rbowEqT7OOrJae2JcenH5qzaod7TpIPQV +v+Ysz8I1wLlC6LzKRj1X99Hng6X+obsEasnPbmEEkuiZ/Sgi4vVC8SHkDmYt1Dx8 +jDgm53oUeWkEJO9LSI2zcrZhSgvg1xa4Q4gY5UUK7gE4LbmGCjFlATuuW/0sryxu +8zxph15gkn4Nqgk0CPMSjesMYEGOsdDzfQXl2tXbt+Pe6mBoWh67MZ1v5zOq3EDt +oSqDpWPxponAeaCuNDDFX44vGjfxGE0tABEBAAGJAR8EGAECAAkFAk2kzuwCGwwA +CgkQhv3H4qESYsvEMAf/VGyqIEcw4T2D3gZZ3ITkeoBevQdxBT/27xNvoWOZyGSz +GYlRbRQrlo+uZsjfMc9MNvaSmxyy4gLVbcdvQr3PF//GxphJ98W8pk9l+M57jfyH +nnCumn7MO4o9ed+WuigN5oeuNJ6BIq3ff2o1DsrEvDChYOJEOeFuWxv+u7I2ABJJ +ep7NbByM2n9PE8vlGU3zUBgWUBsk6jT+klKnEyHE76WzegPLz3jtElTuyB7jRhjy +QJu1yiJEMbs2zH8aJGObi5f8Jum4tILZuEAdoI0M3c3VRq12cz/vLy+9VXa/s//8 +IsGn88kjyyYqOy8WJEjoOXFh++dpWiM7nZkgQcNi5A== +=ggBv +-----END PGP PUBLIC KEY BLOCK----- + +pub 873A8E86B4372146 +uid Olivier Lamy + +sub 1AFEC329B615D06C +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEdddbQRBADRgstdUZq7ceq3NYcR5kpoU2tN2Zvg1vptE9FxpDbL73gdLWnI +C7IAx+NNjdG7Ncdg+u10UZv6OSmhWAd8ubWcD9JxKtS4UXkNPHxhHFHqVPHuCwsQ +q2AaCtuOk6q9OtthQX6LfOuGqwbv9uH/KLUDn91PrgKuHPVfVveiF30ZvwCggutX +D0jTGRHzUJl7F1wViuckHJcD/2z76t0ObSuTnENi0IUjF3Toe4tv+qO+Ljs0knvK +tu1b8A5Bs+kxNcbEqV+zdIph+6gCL9jy+dB9J+t6uZg6ACJexbIkDPsutNtbAVDV +w5AtM7JR8930dRHfEt26ahFohFi+73V8RiA7LrmMjA8rX4zuo5Pr48xt/RR1Y/VE +8ohCA/wOqul9eHHevxeEMDYoGVjGl2EiuIThg4eYuQDDSisBNb9a6dhE8ECQFFBx +mGz32+I8gXSTKFAkkQUI4HmJmTX35nGJql6E7Bn5yM2OaOG04PV+xkhScJll5ZxZ +BNEccFDL/aI4N33cwrLHyk+wFNZHBL1hnHpxpjFZYv5xfEBjmbQfT2xpdmllciBM +YW15IDxvbGFteUBhcGFjaGUub3JnPrkCDQRHXXXPEAgAyqEz3eBEKiZ7VbAj96Ht +IvGufKTdZ0ERJtrdPO4FUGVBcXpphtnPn+JOWomszUKkKLO4x24OaDCG/SENsPy+ +Ned4wjBB+4uV0YEc5Xn8gts3g4Z5p+YiVu+aWeYPPC5BPU61tVqc996i9ZYkZiYO +s9F5Z+dKozk3KwVcijaCr0IQMjAtJ/N70zcciP23KhrN9Z3Nn54Xm7GezD0nxTUG +P8gM79zKHnVhDBptrxIT/adCzU9/UX3UVAQcdq86FfzTEpqFG3TM75HBTQgHihIk +kirzurE+ivh6aaF3UJwmDBe5Wu3gvxF6Rl0Ja/YBNkkCiOXngXSxwvUUR8KJO07R +GwADBggAxOFV2DfMHsTBu++gKJ94L6VjETfVFEYPo7e4tO2Zn2Unzdxz2BoTJcQY +0j6/M3Tl9hCwhOSVVL8Ao/wp1ykjgXnwV4vz0be4d/ZML+KF15x+8730H7Th+aR+ +Ug6K6Khsp8XIypmLJcYgYLD02PlSnDxCq9Fbv0JDlbr6tbsJiVzoRjg+WNEIB3II +rJbTIiOFrRBhloinYoot216QJ1rI2nQpMEBlSuX6f4jYF6F7X4dAY4V4ohjFeJCb +6SYkKbj4caqBA9OVrj3vh8v/vAUKDB8pqVhpaZicFpMd2pEEYVMEU4i1sLE3X73y +9RRuaJOvPAx2HHT8MlWjsDmNdY2Mg4hJBBgRAgAJBQJHXXXPAhsMAAoJEIc6joa0 +NyFGZKwAnA7QdwrbR2IBqxd9SgqHF/4MAomBAJ9fA/O+UMDa7hOEJLf1tEYcv0ES +GQ== +=/u6C +-----END PGP PUBLIC KEY BLOCK----- + +pub 88BB19A33A18445F +uid Thomas Broyer + +sub FF59C22B07640A16 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE//SjoBCADao3lh/I96fWIY2ZU49ljtHR4Vnzmifm3URFNuv/c8McWGxxCy +Y1+oolgVuJcy4hCqcgbkwTiAfBhjZSmsC1QK/2Vs1awFzGccPcgTBakFw/TUav12 +6Zb8y72dH0VxxcN/HUGBUOSgZg9IMe7AmmVnxbJ2ED1I3/opkC6ElPXFOl8EJdgE +Wvinp4ok3mwBGMIexQDyEN4DviuqvmB4K+gYCjS33HtHh4OrkXkCO5pDNUDgkAZK +1uG3GfmxGBjdG6nPWgIuDMEL3j1cW9r5D6I5obXsFlg6bX8mBs91jAtmfTNv+IAB +bwUOAJC+9C3ZEIsZOcBSSdUIXmuRPa51oP9nABEBAAG0IVRob21hcyBCcm95ZXIg +PHQuYnJveWVyQGx0Z3QubmV0PrkBDQRP/0o6AQgA6iTExu1NjbMu90BYP3E8ePWR +k9OE3ujnYD0C3DTMqOI0WX2PL4gVqs811szPCihBaDHljdJsp1IJIOU/vimwQw62 +0R3D/bfC3egbvQjzhG94u5Oz51MNEB3nDyPEteGOb5DGGIT7P6l5WF97/+7X6Sfa +/N6xcwhEF1BOKSMhndblMyC75FXsWB/nNRZMRROezWSYz31c+E5WHkEivWSS3L8X +KD/VaDzuV4zdZlSh7/tudaO75hKCNa/HC/wcQFg8pyI0bmfSg4+hTeOTIS6Alp+a +WEC3cICCYVt+smCSdxc++jDbErfXaLLTEiyUCqbR3Lb4T2OFguVLencnxMs8aQAR +AQABiQEfBBgBAgAJBQJP/0o6AhsMAAoJEIi7GaM6GERfmzcH+wVzLATCgDjKXNJK +xVy2numMNzNzOUPUye8I0/2V3PNTag4YB268X7PMk0vXrYmox3VMxidhE6hmEhLv +wd74uuIFKMKzB4oOoSLHXaa25asAgKkXdRxjxYswHpJeBc+qdlLVzD4+uv41We5H +7Qb9xQmJ5V0o7mtxi7Cuzg4aHasQxEKSwBjkUAx7WVIHiaP2MgYpbQHfUPf8DE2V +C43VpHMYuPvaTp3tD3U6ttDX7IbXYIvBJ3qZLiRUEWOmlQMkIo34cPw0ZD0S3KJW +nHb+CaqyEVV3BSACpDi0q0UjvXduLzHP+g2IeQ59yUd2AwwTUZTaf6AScItGmYLd +CqZ5vSc= +=B+Kr +-----END PGP PUBLIC KEY BLOCK----- + +pub 893A028475557671 +uid Gradle Inc. + +sub 5E9AEEBA28836032 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +WzZKkqBVoahM3qqyoKOy7mfpt1hB4gEq +=E5AV +-----END PGP PUBLIC KEY BLOCK----- + +pub 8CD7D660AE857DAD +uid Emarc Magtanong + +sub C3D943BC53597F2A +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=/twO +-----END PGP PUBLIC KEY BLOCK----- + +pub 8E3F0DE7AE354651 +sub D3047B0BA4452AE1 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFMnpeABCAC+vckg+AqDG5Sg+GKbA5t2knu72aD000Qle1X//SjTvPHz0L1v +rUNzwrqlmah17usczZHOoOCaGjSUFl3nPmBEOlLBh6L4+e2Av8PSbP0qUneaQVgi +TQfbNgRB4v4H5dtKIglK1hZwCeqFazuRuFDWLHl/IG4ymcMwy/86y96BJKWrW+Oh +4vK21DF/BRhyFGaIGwN1aPYRofy3ERsUfwe9WiPXXuYb3gvG++QuiS4V7UJwcAkx +5TGukoatI4T1PVmZPk2zmeM2pHQRisHAScRt5YJ9bswgBphk1xHoENVQ3BYhzrsu +a3hFDY5hO+UQiT+eIE38noOuKuSbRalSPelvABEBAAG5AQ0EUyel4AEIAO6MSdr4 +Sp59Gb+J8t5o5g+f4jMJPm2v7BkJzldN1JISoEWeo8iyCOVcM4D83coihMfN5Nwi +7Tuc4tnZH00+XxYFkHMOLMPtCE7l0Ai8mVhFqE3HraCVnk7gRzNCyXeJRu+Q1TSN +4QbiIEeonBSoGsAYafkAx1evBJtGmrDv0Y42NdocnACyRPZD0usxMARk2ZwQaqN4 +Ih5pL2MGXqMeo6uEW8iIIumnkMywXyZ0jbAcZSs9Smi3AdU8P/eY/Afpu4nyYVAU +Sdm79eMjcBHRluvuk7db3mMzQPAepWQSYOgsbWq2BS/0rMq65M+uWo9MNpP1ZH2w +G87qh+7nFIk38h8AEQEAAYkBJQQYAQIADwUCUyel4AIbDAUJEswDAAAKCRCOPw3n +rjVGUXwWB/0Yq3UknzRomC9wi8sCh9Nv4erqjSP/JSoNx+rYNpwJX74jVmUA4u7p +pzywCwSFKyE2L6pkgKw0y+KfE4cWsotlfO7E6VQQi/+cCb5OCxqf+gOelupuW4Co +MSHKkPWXI/dhM1NMIW77+bLiiHfaOW3Wa9kBSKujiuFSp9tIq2gjTf/2rKQMbywK +szhlBICdvYzji8t79C7tAJ0xNgZJJv0QHP+5MZJfMAARKrvtRP0I5OB1HYVJrH0v +RbO9Y8PoYrPxeR5zQYxAyt36/DE+PM4CpEcCZ2D7Xrtk/GKe3Y/jU8FBifEebuhE +HdZk2xusuOEx27cIovRPHwvLcgY+4u3j +=oNWb +-----END PGP PUBLIC KEY BLOCK----- + +pub 928B20E9AD5298CC +uid Jakarta Contexts and Dependency Injection + +sub 0AA3E5C3D232E79B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +s5E5s0wc +=+V3C +-----END PGP PUBLIC KEY BLOCK----- + +pub 928FBF39003C0425 +uid Spring Builds (JAR Signing) + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGPF4agBEADsWnQn3m/dMw1Q6FLALQCEucRkRLw4wcmtzXuuYb2M17aHvEb5 ++OnXDdgQ2QcnnjK4S3zQp/Jt4ZQnnLORqx919+c+1gT24U06VxeGPW/TLRn/hnKA +ZM7bwGCz7em/GQ7JaIXPn3aqGrCiy8j8N5jAQ4ePP5ESQLeWisQFlbKMFVU09B7t +0IPG/LQ8xJs/SveQw8sGrz9zqyYE58EauyIKfiI91Ruk2jMcb/m+SRL9Bhen3Q12 +g6kFrHurLtEWzy1rwELt9g9OILv0FokPa2m0goQRHx4nFY0kIpJ/r9kDUg/tlg4H +OxP+5XTJxEXGW2gb9zpmDpdR8aUgi67/Kdm8+norzpTTMAuKCgclCv3bJz2D8Jnl +IgEXmSoznn1EnFkm0Qsr8JwkaPC6LkGcI7YqvEdCXtRXduot+9GgWytp/IxrlJ4X +vFEhOmChdVdn5lT1T0ka6NxEJcmf5mfgyz9jRUVHINNJxQKP1Gh7LyMFeWw6V/X7 +RAeU/th1pL2rAeBqn3gnK+CIR82/AVDJJa86BAHiQBYilgs+bGYriXYd0qmIFvYO +sSfC87Dbl3kzwcAGWB4d6tezLxrzscfbzWFIa9woy31/OcqK+uglSKbTFsIklhM8 +Rr2B0tQS1iNzq5gyWGKnh8FsqcAiGvDpwhF7irGhU7fNRVVSuamVDnV0BQARAQAB +tDZTcHJpbmcgQnVpbGRzIChKQVIgU2lnbmluZykgPHNwcmluZy1idWlsZHNAdm13 +YXJlLmNvbT4= +=DLt9 +-----END PGP PUBLIC KEY BLOCK----- + +pub 94B291AEF984A085 +sub 9D149DAC4AC24632 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFPzzfABCADK/wEIRhUCUTj00TcBOxGTPs5ad8jn5D01P7P5ILpLOgmnUp1I +E3EYy54PQYjDIeOFvEmEywvwMRV8yCVhhYGpOPqbegKwcebXoiMGhJjuRf2nPbdZ +PSB+S3/WAsdydiPiz/2Xl6hhlaKIQSnSOgYPOQjbDjgfU7B0vYGPohYR50fbOd9D +QLvwsYxQv7CCdMM1M+tx4HevvYOKrceAwTe6yRx9PEhmuXYRCes/AKOs7yODvNm5 +SFFlZzBrYMxh6LSmCAGfYrSGWJliJUuFMQ9U0R304nmVUo3rrCj3tD25Kdr7wj+z +WjtJVBdWVFTq2/Zh0QnF7mbIOs4bYxLlKe+HABEBAAG5AQ0EU/PN8AEIAKo/Zv3J +TODzEyEcEZA263xUgE2c5zMYFHvozhkW2YHphHdC6Wtg4K4atYXfIzkIJvrLgio4 +SYteaChzGB0ceN+XBPIL3VvwvEfk/CMHfgnuSLmkSgYrIHCKQnEgW2g4GKhrI4Tk +/FuTf04KqG3q2tlr5sAxp2nSDfNkUJLx/UsqY9nhci7hVJNcSCgktuWuthGK9T29 +v5jcB74BI5LMFnqFkWvAOsUHr763Hq3I/2uHWAfJv0UEIXWqaOCd4IfwUMSSBwL4 +1ccVsi38jiHZ1drZBgsrJdeeEdMplRyE/31yC55FDyTXgEMyEzCI21ehgFJJVa9b +I7XA6lzxwJePQwUAEQEAAYkBPAQYAQoADwUCU/PN8AIbDAUJB4YfgAAhCRCUspGu ++YSghRYhBB2ap/nh4oJHKLjNF5Syka75hKCFz30H/A4I9FC5kU5ipCv4iqc77egC +ekoG8hhm0DtVdWrKOgJwbayIRsyCB+hISnZpt69QyZi5iA2krIPZx0Sq1Nrlw2lP +lv87CbreDy57vdtMdFEFQHW0zRbFN+XKZ0noFQGYxG/1LyPR4AOg+ykBEX09gnWY +HwUO6x7Q73OQs88y5JOurF6A4iQmH7na9Qz2A0YPWNKQ+tmFMEciypk7/YABFZrg ++9Edz/TWyN81EERhJPDkxzHkYLm8fLpOhnQPOyDxuzt7fh7yhy/+b3B9QT4Cv1yH +73DYIfJW3jtuQDdmvtTk6G7BdEzAWufGVt6EiY10pr4zyfpdeaSSyYoO8iMWJxQ= +=8DEL +-----END PGP PUBLIC KEY BLOCK----- + +pub 98AD2E19BFF4106D +uid Alex O'Ree + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFv3+7oBEADXMQXwYMxs45ePjno3XcOouM5NMCYsKk6qmrCBr2ffLLTPER6M +9KtyYAqCqe1DQmB0BRau1GuBRN6mydzILjVaJ6B1S+SsDGHOTDpTVyKUKSkQ292X +KzGq3w5xqvQ1ZP1Iob38XvGQDR2vKnqDeZhVcCsDrbMPBpO3bxCCwrztwHogGZiA +wejypOGlYz3stRY7SKvFkuuH75An6KGx/C9VDh8EH0tUT3hQ09On1/m6+I/8X3YR +WUhCj5WuUg9oaI0MCd9CjgKRH7PEnemFhb1iXBxcOn8vC8hRmYqsl54gIhfF9T8P +HdyTfqji7AKNu3zz3yiiaqlteZewxFyjhwnjvmJMQ+D5Mt9ZfexuDIMY7319psiY +w8x8CuDb1NPq3vR/P0oo5baldIk2r3K0uOdXBGOls1bbqUTv2V+fFdAzk7qC+kxU +PrEpkFKMKd2jtkcO/0Qfgn5r5r7aU1AyGRbXT7fySMT2/6GFu4XSrVWBtjFP9u7F +5ELb19dFaQk6eUCr+ZMTwz5TP6pajDp3NSBX+lXZipYHQEIcwk0P8kSwHwqVUhgr +uAlyy5E/bvmBxX0nFZVbGwlUedT2OkwhU2FeQKfMG9Ohimpx0qK0XQbRRAaJ8AI9 +ws3rC/RkPdfOZvgIdGScf+aNJNb9/5pnSUx+bpBppg7MhbWTnRkkCR1qsQARAQAB +tCBBbGV4IE8nUmVlIDxhbGV4b3JlZUBhcGFjaGUub3JnPg== +=pevr +-----END PGP PUBLIC KEY BLOCK----- + +pub 98FE03A974CE0A0B +uid Kotlin Release Key + +sub CC3328A2F49A80C8 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFhlXQUBCACoN2nTeSRVZnGoktKHyiCgeYQ/hEKKKDDAbWubnnQwonCTILaN +Qw3GmIT6plmi9iy4rl+rJprSzDeQDZngQCx1KPYcXCrrc0pnjERDaogw9fC3c3z2 +B6+90qT6UJSTNmxMs5zbhgzKDWb3eaDmVDqVqzsM5xz9GxX6zo83o37fTFSbVbtA +9+c100+KaAldVL/6uLeGoQsAIxtMH8GiOPiSjrw+XCQ5mbP6e+oYYBKxEyAgu1XW +8jP4bF0rz2+1lkIGfWfYHZmMbmSutDxXqOXA9cZomhOayOSe+iczoxXkVXkQzMxq +bG4ru5CHxPh+RSfpwA+9StLvzLeoFrBUlqW3ABEBAAG0KktvdGxpbiBSZWxlYXNl +IEtleSA8c3VwcG9ydEBqZXRicmFpbnMuY29tPrkBDQRYZV0FAQgAo239WGQjltcz +GgzZdgyH0w/PruwBqpz0SLFqf8441+3JxK05UcHRiF9D2Ww14shvqpC+96yS0jV7 +G/XzDFKk34ATCugwYNIN+LGGmBp44bOPyiZc5MFnsG/vcD7h0JV6hDUX+yzg6REr +snHmMsztO5jsgbrhdJjLoPpC8w6gjEp2oHzV/olgPNXCzMW9RRsOpVAh+Fdy4IsB +tjG0lVXMCV0YFZH/vZSrycTd6t555mMeeIANrBT5dHYSC/VowO8/zNSlaXaZHM4o +D7VInJeEDnr3jfmzrTg9/E90RKUqcxiWKdZk28vmutFXXm9hhTa7n2iX+DbsM4XL +fdQKsKBngQARAQABiQEfBBgBCAAJBQJYZV0FAhsMAAoJEJj+A6l0zgoLY3AIAJMK +WFxboZuxFpEfNJ2dcM9Q2VZ/RQM/PRqY2nyZDCzKAv6NbRrC2t/HFKpu/7WbIxAH +uDq0sUZxMf6Op+QOw3aulDb9yetRWa6cyOQKehLKGuqiVK/2jDQvsr4URGbGrlAa +isT8bJrzugrfyi9dmYZSzv04ZK0B4SddeS1r50OVfefJMYznYcRpps8lG/94yivJ +k+C/zpqFqMxTFtz0t2OR1WfCP7FrlTHplW48Yd6h2r4xZNm4oz/+pP25ds6yCIpY +nUTdg/LginuRzid73nRYad3O787doGV2qzFSy1DRptle0BWPWPYij8mC+31u0T2L +Syx8ZPwcJq0wXCx+L3w= +=9nKv +-----END PGP PUBLIC KEY BLOCK----- + +pub 991EFB94DB91127D +uid Antoine Mottier + +sub C327DD2B96A50E1C +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=QLy8 +-----END PGP PUBLIC KEY BLOCK----- + +pub 995EFBF4A3D20BEB +sub B89991D171A02F5C +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=AWd1 +-----END PGP PUBLIC KEY BLOCK----- + +pub 9A259C7EE636C5ED +sub D66472CF54179CC4 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +DG71mmILRI1wVgrDp5mFKeV/d20uMvMq +=IIN3 +-----END PGP PUBLIC KEY BLOCK----- + +pub 9C4F7E9D98B1CC53 +sub 32E3DF6FC5E91334 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +EVbkCS1SyGvZEvARpZ9Z5Bnh +=hgfJ +-----END PGP PUBLIC KEY BLOCK----- + +pub 9CD8549ACF9BD0CE +uid Tatu Saloranta (Home mac 2013/7) + +sub C03A239659D683AD +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=8Zvz +-----END PGP PUBLIC KEY BLOCK----- + +pub 9DAADC1C9FCC82D0 +uid Benedikt Ritter (CODE SIGNING KEY) + +sub 923C08F9417B222D +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +zW9k+nXG/umtuGWBaZVODvhr9hDoUpp2+qte +=sNEF +-----END PGP PUBLIC KEY BLOCK----- + +pub 9EB80E92EB2135B1 +uid Slawomir Jaranowski + +sub E3F6790A5A167F5A +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=hVLP +-----END PGP PUBLIC KEY BLOCK----- + +pub A1AE06236CA2BA62 +uid DiffPlug LLC + +sub 030DD9087C31C9AF +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGPAYVUBDADCs9PkY8zzhzE38bRZX+vTrr3LdChGNBmObV858NGRrXeZtyLu +U7YwYVF7w2vHUI/JQWrlPZc5tBFicefucfUtKt790WzAk3NBjGtX4IWpG57C1Z5t +QRI23HqWr1If0UanH2vUjy4fUgNZdYlH2KzookoU0950dIcqwA/HwiosO5RmQ9iY +HztkwwvFW9QxiJgL+lR84EcaIVN1ukr0ZKrG1a6wOJ6HLf9S2F3DMe7fQ+O+TpT/ +A11RewOZHwE9spH8cEsNYgutBouw/MttuYHjZKD7O4hN12MmzecWeMAPyrvYgTJp +PHcjQaVeD27OGLRpy5n5LonvkzJbhTzM+Ps1qEj/4of3EFVhxNjR0gdrkX+0Ub1A +XXiw3gjqAQuLQVKIbwvCbfj2go+YHrfcXN6kpKkYZU8ERPxI/VBTw230PPdXXwXP +Ux/JRQIEXOuMIuELm/91H8TXKutw6NdNRu5q+LPcUkU7W6jsv28dmQHdC+ebVHlA +vNcBOWVSY5e8IasAEQEAAbQjRGlmZlBsdWcgTExDIDxzc2xjZXJ0QGRpZmZwbHVn +LmNvbT65AY0EY8BhVQEMAL4ZVuEVH9zbhY6AewA4T3u2XZ7k1KGOxoK74eygzYEp +fKMplWQtCxcxBXe2tboT7I8U3MrV6m7KDwcxLNVZM638fvfU3Px0yGs0jBzyjOcb +Vk6n18xX9UoNyoEqpxHhyPbTBr+U9OINcneXZ/iG9FfLURZjDxhNcQcnrnmvbUB6 +M+teZ6Gpb1Ye8ghVCJJNjRRQhFxXE7XnmX3C1pZoSoGcBx5zVspSuHjq7nTTw/rd +7OpC6sBK0ULk8GPAd2vJUfOtZcsLvOs2++bHxNULTXraTy/fYvXsTSe+PmbJo1Fd +5o3imI0eUy853UJmF/HbuWspFe8yONHjo0+uZITsAMq0jbzG8MTKMmgsXS/i/vaW +8BVUmLfhB7E+bUXJLJuQAan397NbYZqPF5agLZ1wHSki3iuYEttdMsy5PYCLeCqh +8Tv6VBNkEToKDAvNbaad4ZgBdwbBQaAIrWekWpiXSXizyGr/VTFE2hT5NC0d1BXy +sc9P2UCvHit6A1bCl7MSywARAQABiQG8BBgBCAAmFiEER5e09dzEbOphBZBxoa4G +I2yiumIFAmPAYVUCGwwFCQPCZwAACgkQoa4GI2yiumIHXwwAh4/tSXSQ9Btws4ZP +eLfihAb4ogHOsrJ8ZO+lZMyQOrEyzDK/y/1LpFVlHYEP51XS5h4u4XVivXGzsZ+r +tQoXaCS6n19dyyNeusehZx/BxxQrdV9OYEkgb3BC+05AWogdHXTP4prGdMtpSttd +gcxTuHwx9RUv/d6CsQ8DyKyjTv82hd3yuXQVl1829NwDbM7HJ8eq0uZPmez2ewbx +Ze9CxjKoOLfYSQ4k0DfcIFqz8CSqTVIz5aNLLXiY6NXPhS9B9/bXkRNAXzUgMrG4 +GmmP8XLYjBn9g8V+fAad67N0dUWDeAPzz3OXjp6bxyScgjT6OMlp55xXaE5HWW4a +aE9epjKjLuOD7LYdmv0GI1HhSrOnlqznB3TCwJgKMw6/37uGZnpsX0JoMs947ZIm +pcN1kNNR3e4aAFcpBwj2OSjds+G/DI3/WOXJj3aaRI4nBRr2/IB3TVhzLOizLTNQ +Q/IKL5Iy5doINK/iyjb/G/JLH1/TkhW9zEheiKUY6TiXeR3p +=v9Tm +-----END PGP PUBLIC KEY BLOCK----- + +pub A2115AE15F6B8B72 +sub 6366592024774157 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBDsSIk4RBADSCj6rUjV64tYCGT1DYKYR7GthyWpNdGHSYLbETBcDatAe1dzQ +5NsCgfrlybfyeY+y1lxr3T9bqf6zJWDw/718wff96qmmv1qzexSYtmIrj+h53V82 +EXwWOFuYMJisuxdT940iQzosm3GOv4MJdEg3oI2SgfEyRQQ6vO4Ob5rHDwCg5taZ +nrHOrXx2dIGHxpxRZ0SUl30D/jmtttFjYOQ3LBMriikz5mh2sK3ZnoSRF4o5O0zW +Ve6e2SFXOEjVjImKsH6KCbdQNelrAdgiyOoXClyQKsQ27pncbdWo6bO0E3POJZVm +XaeW7iudHVr63rU5PViXObIQrdQl0D59j5brKj4vdlTyUw8kaHPvbKPDEOwvZq4Y +LJQ5BACA1YilTeXRJqwFsNlpcxCHwlULD4QUVP496prQWf1B7Z6g0KvLGrQsO0Vn +Jcn+fEqukysTJixSXCPebosltd4RalJIupVYkp4w6MJ7biaDAlLuNhDcI/AiXTmV +dXUedVXIaM8I3Ne23gucwbAyc0Hvb+3cSAKRhl/azFQhuHBvlrkBDQQ7EiJUEAQA +zVKWS8QrkysydbTJu2/14wIbz2Coi93aAGelwCwXSxf50JpYdY3Lkcvd0FqT8bcE +nz43MCSx8vlKubQtUpx9WMGIb4ixtShLJ4lAa6FJldhychz/dnxSNyz5N8W6sby4 +dTVxac0rloxjAOurGanhG7TMtgfDi0cEEoXRyAVoKyMAAwUD/j1pJm4Npq2mlJoE +7MK3vAhgKwYHFflmJusmqvSAtRuFdT15pbMJrA5bAK+lA3SVOOhhWTCItlphSR2q +xJCAcBTeOMqUi5ohFcCkSRNvwmDtH+80B3BehlEsEKNk8Z3moa2ch7Oxnb6XEXH5 +tGJ5Qvx9Qid6ZfBaXx7bc8yKyCb4iGUEGBECAAYFAjsSIlQAKQkQohFa4V9ri3IW +IQTOgHWiUVR77iSbwVGiEVrhX2uLcgdlR1BHAAEBDTIAoJ3NtpI/E345LVOruElF +KrnduWWXAJ9Adm9Mz4yoxrosHSkp5BWzXBUt4A== +=AtDT +-----END PGP PUBLIC KEY BLOCK----- + +pub A41F13C999945293 +sub 8183E80D264EE073 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=i81z +-----END PGP PUBLIC KEY BLOCK----- + +pub A5DEF5A76F94A471 +uid Kengo TODA (GitHub) + +sub B15AD8EFBD28D3CB +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=7Q7D +-----END PGP PUBLIC KEY BLOCK----- + +pub A6ADFC93EF34893E +sub 9C4C23E6FFE405BD +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE+xZxIBCACzKctn4ez8xOC0pGThhAwjYWGkzcwK4HNaC1usHThBFz3/t8JN +OqUXRixLyi5wELN6GHlsGVUQS3IfB4JtuhScsieSB8PTree68/knMq6JI08mJqZr +9nFrAB4eDW0UMbSL9kPmclUm/yN+qcCZBrsVn0q6CWb/Kcd8EEXEu6sGILzOGqGe +d433t5O+tGXWL2TjAz+Scsk2Hf4zcuDeQcxELAMnVaVgKuGuEZvibrjsdIvJDGI+ +0BzWIu8ZP8ldBl4SVtzGpEVzLvDUo3mOqBeTkj3rP7xLtFDN/3AFtowbLfL7L2Pg +SMcTnKK+jfFHRfbHP1Ih3rQ4ilLzhCnY/QIZABEBAAG5AQ0ET7FnEgEIAM3i3e1s +jwrx2PN8XYMPQWG+/YTtw1BYDl2+iYE+LaZvtq1hpbgeCLgEVwXrCJ4spLP1rFXo +gWqKrkJ0LRjlpdKhKBvyH1ex4grh3cWN/bIDJcJ7JA4I/Bhqhlh8hYycS9pGFeS+ +MR3aFIsii+vadrwYYvuVYGeWvdZhB7mJKYevj5Ms0OpYTfZd95Pzo4o//lNpDnrG +7Xd3tgTNU/fkpw6rFB/2Ib1Qlk+Kz1z6JNsp+tOPGGCBrzwfwglcikTuqS+xyRgC +9cHh5eCol11uSoWPKcQR2Ar8Eo56nxv/UApdu15iJ7R8cA5guKeeS4jt0CGCPs2P +huggDxI73Xvl4zsAEQEAAYkBHwQYAQIACQUCT7FnEgIbDAAKCRCmrfyT7zSJPuyl +B/9iwtIQeexMWBmQNdDe0md8HLulDfcujPtklrvYHtXMJQFaGA0Vafq0oT9MhBfb +1YCP79uF0qgswSxINYCOJx4nTPIP9BOdTwqfGo7ul27REgNq4lIUW0GkMgZAUA2f +t/vc0u/I0PqnhKCi4Pq79hLIx7eiX2ySfXfYfLXRVzbMWKMoi7lWXseQqbM0RvCA +54J1qAi6Ew+JyoYGQ7OvXdL5Eh5Tkm2cpIADyqCkp/aFDe5lqZiU1zS2fU6mpOf/ +o0co+GoYkieIxxibDCmt3BioLgmyzpGUsMNwh4pAIQUGkcxd4spC0KIWdDEvq/QJ +EEIhZlI/ojefaZkRseFrtl3X +=pJaU +-----END PGP PUBLIC KEY BLOCK----- + +pub A6EA2E2BF22E0543 +uid Tobias Warneke (for development purposes) + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBFJQhigBDADpuhND/VUQwJT0nnJxfjAIur59hyaZZ3Ph/KIgmCneyq7lzYO6 +xa1ucH8mqNBVNLLBhs4CjihBddU/ZKTX3WnZyhQKQMZr3Tg+TCNFmAR4/hnZ3NjZ +N5N5gUj/dqVI2rIvypIuxUApl88BYMsxYpn2+8FKeMd8oBJLqFRJ3WNjB4Op2tRO +XRWoxs1ypubS/IV1zkphHHpi6VSABlTyTWu4kXEj/1/GpsdtHRa9kvdWw7yKQbnM +XuwOxtzZFJcyu0P2jYVfHHvxcjxuklc9edmCGdNxgKIoo0LXZOeFIi6OWtwzD0pn +O6ovJ+PL9QscMdnQlPwsiCwjNUNue20GBv3aUIYc+Z8Gq0SqSan5V0IiKRHMJkzd +FAhnpkSFBvHhPJn07BCcb1kctqL+xnLxIdi7arq3WNA/6bJjsojc/x3FdIvORIeP +sqejhtL8mCBvbMAMHSBrFxclMp+HSz2ouHEEPIQam0KeN8t1yEqIy3/aYKMzHj9c +C3s8XOaBCbJbKpMAEQEAAbQ9VG9iaWFzIFdhcm5la2UgKGZvciBkZXZlbG9wbWVu +dCBwdXJwb3NlcykgPHQud2FybmVrZUBnbXgubmV0Pg== +=q1C6 +-----END PGP PUBLIC KEY BLOCK----- + +pub A730529CA355A63E +uid Jukka Zitting + +sub D5A25EF82542C54A +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEUQYOcRBADsCu4zTVaB4TOhV7NyTvHhG1bqN+3Va5t4vpGQJg4M4U0Yu0ut +4bCZP8I6rlXGj+TqDKVUx9kfGpIKX6Kw2TvZUYbHIDWh3UhQO1hD4xy4b8rOak1w +3vDAMYA5L/jsZshNHp++aTgmvSoXGS5S1xsjrbrOics6iTtHXMV9TCRelwCgkwGj +WHzPJnlSj0z4jAlRG4ZubAkD/3LOfrXtti1oirfDTnBZcxhvldyCT2yiE3LRe8N2 +ijmtNO6fl2fqXSWuP0L125ytlOvww1r6Gd8sVXiVwt2oKZVQ+A5028BbHa0u4e3y +54nA21OBKLCC/hJvyOkPf9/kZk6S7fV+Tour/auixX4WqUg+siMRe/EwHw6bQDD/ +1OK8A/4rVPPCDTLvcQbT+B3z0IEfryMkivJMu7dEoENDXDK7N5KginugnCpJd+g9 +GbYgTYI2YPNB6A2eaR4lH8yQCMyDXC5+bGL+1NL3SP1qR4JE//nUcbx+iMTYR5uX +kwmaGMXRl7z47OgtaWM/dVipNuNaqkD9WkuMGb4rdSNHrI+amrQgSnVra2EgWml0 +dGluZyA8anVra2FAYXBhY2hlLm9yZz65Ag0ERRBhAxAIAKR+w/FZMViYB+cwjuyN +ZtiEwX/KbvzHDAr6Qh0e6oEKeMrywYkPgF5G0D0T58TvL0DZUi+8Dg32CPYSg5KI +uaRTZc4QSPYr3oVK33cd7jCI6MGZPlDAYWoh+D6Ws5MNxVeyYSptKwPVV+R57oxp +mY1sphPslUSYUfXctF0KPm+OvdVXvAnvS+49I/AcwnAsAIUzrRCsQZ4qJ+Oii4K9 +HGg2vPQtRxvECQqkKzfh0W349O2bOB4yZdgriWOqpf39RpRsSWGU9BcXyLLdNXQH +TRaaQ/CSdSPj2BK1nYJJta0A5AGVxDe68LYoR4+gNBBKq7NaIJF88l7NOXybq1LS +Of8ABRMH/R6r9CeZMs0gN5lYUs0mNaToXolT+qVligrGesSzPdD1npAB7BDb7xdJ +raAADkuh2CqIOagxyZnu469dMHWpcp0n4Agws7daptnb17xJOvY+76i5qgJChIq/ +/9V2j5RisQNxGI5eFPB8XajocPxQKVkZiZ3Hs6nhPkfrg+KTLoIuwy51WvdIZT70 +TWHwbrV10vKe3YbogmuXmz8XJb3Fuhf5n+mQ8A0roZHXTw7uzg9+nm5og+kRycLH +5/7js+LERuaIGDaogeTjpCjucwVc7YHJlnNwAszQm8pIpw5jR5wGnoj1kd8U6fGc +DWtAQyxgV+UVLFIT+e54UK9In5Zbm7KISQQYEQIACQUCRRBhAwIbDAAKCRCnMFKc +o1WmPqVYAJ4y0DxSoOkHa1ImGAkGnLXNhsfTpQCfa+ceGT5P2YEw5M+OBH2Uut2y +diA= +=7rJR +-----END PGP PUBLIC KEY BLOCK----- + +pub A7764F502A938C99 +uid Jisi Liu + +sub F20DB7FEF61CE1E8 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFc7oMQBCADaIPEUzMrwF9gnEC+PRn2cSPG8OV4RxXxa88TZm0L7NF7D+F5N +MNUAZ58oVqFUW+ytgb5iey3X7KjlJXZnuqES4m2Id4N7FlnvrmpeOg7MUc9VmNkt +E7RH0O5GAo3V3o/Xp5nATUoaQ7v1WaPloB78ucyWLnH/iANw8YuxYuYUTJ0f2b15 +1oY1feqkpibbbO39kdMY36FH5h8Gsv0vvGdeS2O1p9vbzDHXEMLkNyKfcILLjbNU +O/n2FzDL1y7hHXcFKDl6z2G7rKJCAsASH+r51YX3dOKUepE3pFcUbMAXGeG//n4B +T8cv9YOhsYGaXBdbOMjy1WsZMvizbp0p6yIhABEBAAG0HUppc2kgTGl1IDxsaXVq +aXNpQGdvb2dsZS5jb20+uQENBFc7oMQBCADMbSSz+BBh63/250biR5MjKnfyxCDE +eCrM1JdQFOaHgsWoe2bCtjI8AFAw9lHiwCMgDZiqvr17ZmtsL0Hjs6NEAzJ8S6gA +0kOMalFHIG5+9a4zKtjv09OgSPS69gWl3rmGI4nwPpaFswmzh3IM+/KfEUZsZQoT +uZllpZ685w21I1eD4ofDNS0f2tm1IlPxbYNeBj0q4XLfQ92+QozCGuLaljKbyPuo +zoyZv2Q91Me1m85Rwfgff8k8O596S0jF4/9OzySq6bVHiTjaa3Te1d8INX51UvAr +kJiyqCYVNZC57nIJwhUDGh04YY8XWgTK+jJat7q09gUsPe9AAdaleFoPABEBAAGJ +AR8EGAECAAkFAlc7oMQCGwwACgkQp3ZPUCqTjJlIzgf/Qcjkt4V/cicrAPyA2xVq +J2JpaO6doUG0dbYkNN0KW12Vwj8ZEpJ1trh6hS8XeXhZEeOhMVyDahczDule/qPr +KI+41Ditq8UFCL3X+Nt64Y3TlPkxl0g1SnzYQXPz/s9MUF+05DrjcK3M7YLJqEiP ++StK2xevmMd04c9vWTRT005HRRoquhnwzsATqdFTrzch8O5juMeP+YHTyie87DE0 +9ToAWPlEPZrG/2nuOx7wBCIBYOuTgGplPnFnuO9T0LqPDBJnkSgrCbmZWIx3MKr6 +nFTnByfV847ogj5+De2jZEk76DnBrwVmgJgU8VlUTIb7Sa8nZZ1iZQiYj32b03j1 +EQ== +=u6QY +-----END PGP PUBLIC KEY BLOCK----- + +pub A929EA2321FDBF8F +uid Norman Walsh (Key for signing oss.sonatype.org artifacts) + +sub A0F7D9615B3A5E42 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFjw3rkBCADI29PsG4+jXT0KajnjFSb9jQynKlHeMmp59e/88goWICaTK7JA +lM2YB5vpjBeQjA+MCv/1wRyD4defs/cXoNAAsdTeJkh6Qx2C4TyL2dKROJK2DixA +p2ENJNICHzKQn44UFZM+gsuhDvS7M86xsj6BCKK0JAIHPtscVSdiSlEvRAkLpVQA +iWLwhTtSju/hMjd0uPWWyQNuSR35EG/nFS8DvxOgblKKciT53CJ03HR3TWtq07XH +wMzQMA/GfgPbzSYD3imr16Rjk1nWbZbrS7N1bpFyvl1qNI5uE84AqvJlzZf/pGNC +PKXNjaGwXyk6OtNLrHoc5PrITava34y6QyuTABEBAAG0U05vcm1hbiBXYWxzaCAo +S2V5IGZvciBzaWduaW5nIG9zcy5zb25hdHlwZS5vcmcgYXJ0aWZhY3RzKSA8bmR3 +K3NvbmF0eXBlQG53YWxzaC5jb20+uQENBFjw3rkBCADDWnfHm4xiR/I0/CKOiiu6 +gTIrvzTncmSWvtNjxMTvob0DUHtgkaEak1NvWudUCqf3oK5zJWb9uhfioHtjTW2L +ZeKf7ufCfnEZq04242nBuXK2b8Ab6zYAheav15FhGSPlSLoYS3AtmAbgUYdxr63h +9R0bZeH1r4QzqippVftO95qhsfPCGf+hCNOlnQPSaQN4UbKg4KJs2bwT1e/ZC/Py +P/Y+PDs1TT1tr1nmcsrwKndJ83FDGvZe2c5oHRXVjH7uJpOZmrBnmuJJPgpq7e+X +0/ndH0q73E67cqwmNYXT2ZiwiwIr08sFN/YxDrEd4nGbf6HjXGN8gzc5pZCpEEXx +ABEBAAGJAR8EGAEIAAkFAljw3rkCGwwACgkQqSnqIyH9v4+U4gf+KooVkVLHnsO5 +RBD47TPct7izcVchMoDLIESmWmQfska9TPcvCVANAbIMhoTVWTt7qsrfow1dMah3 +l9fwBCArcaLIw/fr/yGTTb7F51/8zrPZ0RScH/rA3KTpxkgBR5t4SIwsICtoVTah +3xh9izat/W3zkhXsoe292NltTXg1o+j6koUEWSUOlyKJFKw82YoV8hM6bgUDatQ1 +S5sCrY/hSoCkbJCQ4anYKBK2sCe9W26zYBbWj5PoYXI0C52vpQMPurQqHW67AZB1 +ZoY17lU6AU7eIcv4F4XrZ97c7MbI0osVgx6b2mBgavQjzGDH2IiQkAuVbSC2T5NX +TiReV1ObFw== +=lTDZ +-----END PGP PUBLIC KEY BLOCK----- + +pub AB2DA4527F6FFC0B +uid Egor Andreevici + +sub 1A94B14C6A03458D +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=EiTD +-----END PGP PUBLIC KEY BLOCK----- + +pub AC7A514BC9F9BB70 +uid Punyashloka Biswal + +sub 7B92B768F9D37337 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=XaPu +-----END PGP PUBLIC KEY BLOCK----- + +pub B0F3710FA64900E7 +sub 7892707E9657EBD4 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFdbSfIBCACrFI0ai/abnV2U2Wa9QQZwGk3Fegc8laiuTKc0GoYdyptd83/H +hD5S61ppdkOugBjVTHdgda3xJ7zBZdnwjZvV/TyayQltbh6hU+BMlEolzXLgyvY7 +cAzKE+iKWbLLwfhRn1iuC7s5l1NLPsh44IUt3xDaFXNQrPO5OnRz8bqsGFVawxmu +2bPqIjkhxEiYpxwaZZbDkgBR6rbBth6A7QOadQcj/9wNdekoM9dyg+olOUmnLrtA +nMBhrvvbm2fZxTps3SZHlLV7+iSu71B5SqU/kT54/49n8vxrQiGvzp9K+t7c7EP2 +w4Ax1nYpRkCxYdHOX3YBdayUiP9ZaYH/YHtLABEBAAG5AQ0EV1tJ8gEIAJVavNan +4WxxlwLwvnBj3/wcEWqN+kfMHENMSjmRWOYSmC332hhGLmTDi++BPWt2OOvHUusJ +V8dZP5D9yUBRFsKozIpyXyS76C5VYGMY8WZ6kyqn/mLCiwmnkOJ24kXLaaHPsQjv +6i5f2KliDVhAGUHmNMJgH8o/GL7zZ03Mb8ZlKFZobp0dn+/lxoOtQSzR+cBz8NvM +BkOKD8r4PJA6BxCR1HVEHsq4xSnjr/UZOYvh+Kaxfnop7Rn9in5MoY2rCY+PV59X +bx4grqNpjupyHEf1MHodJRj85JiClnLZk7dNJ/kr+zggwbsd12/GHkBt/pxuWhe0 +eFcAOJmvqC3c4pUAEQEAAYkBHwQYAQoACQUCV1tJ8gIbDAAKCRCw83EPpkkA54FA +CACFZB2Tk96FQkr8+WHOz93CJs4UD88PosLaKmiXKP68arjH3y5jhNLBzqteZo0C +rfw75DYWIZChdf5uLGKCWXBEytF4uoHOy9Lv/3emoSeenluFVcNjL7CIOQDRmqw1 +t/LjnsLbgvlwHix5f7I6Txu/J0HKJbq0XpoTqCzFK6sxEPHH3gZto+XfHk85haKd +73SOM4edkmJx+jDXES1wb3K3SpYibt+uPVfLYXWxK7xAaztESTIqZ9RnYHzd/7z6 +DO4z//lfB7IVAqvM8ga7Qj58ObeqZxx2iVit5WUZ4cE30crNGyXH/HKlAp+B9EvE +Nnwr++TI1CGYthPLFHFc831L +=57TL +-----END PGP PUBLIC KEY BLOCK----- + +pub B16698A4ADF4D638 +uid Checker Framework (Official Release) + +sub 32784D4F004B405B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFM1v9ABCADD0KoXq2ZKlUHeIVovQy3gFmW9oFAaraV48ouv8cYvqdf+s91H +NyqeyNPT/ihFeNqZJUAMyPdwN5xrWD6gxMrOCR7BFhA5kLmAKz4HfFCQ05ViyQdI +/HVNFvTdF8LNnuF+a5aNgg+jjLvFwzkyMFkuiPGuUDFnqEGxC+z9J8t40tpOTOIw +tPjSzkDN41AJDpUK/simKC5F0Im78nUbwMalE5z2IsZRWpYZyIhN1HhEdDvaDIh7 +3vENjH7enAjWh0iGRu+GTP/fayZnX0uhmausCCwMMhsr489e63ZOaJrqeC//wWrX +dtEJjcmvRmJ2hwLmgwMP4zSNKsnLGzP0sh69ABEBAAG0TUNoZWNrZXIgRnJhbWV3 +b3JrIChPZmZpY2lhbCBSZWxlYXNlKSA8Y2hlY2tlci1mcmFtZXdvcmstZGV2QGdv +b2dsZWdyb3Vwcy5jb20+uQENBFM1v9ABCADutkjG6oCMxBUBB5cTTeaWR3e5rKgx +EiCxWBZCNZsZZA9LcBVjG5OJzB9lV4Yrk97paigTlFFDUKzu3oLX2xrIFb+G1m1B +33mZH76Fg5Zm674tWC5Uf2ccxqQjXPHt2jnDd1yh5QcH1GnKOqXEwby6SjwP0wI5 +EzrSuAOQM79QksKc0iX9m1VW65+5ov68O/EpmQFdv67YjlOWvUvt387MC5NTzv8/ +/3eFaAnC9rNlrnlTtUPfZHo5BOeZd5WMBIgc1bgAPfENGucIPOL0RhWUFiyMPHNt +Dp9vnWXEy2XOtWY57CNS0py1FMkP38x0Pgcp0BfZeN2QjyhSJdduTBopABEBAAGJ +AR8EGAECAAkFAlM1v9ACGwwACgkQsWaYpK301jiXpQf/bw3Nxv5qyBwdT/85dBXZ +ecEM2klXPSQf3HtNNfKbaZS+9dWn9GQ71qpmZCTZGLtJR4J54mlwdJdxhlDyGv02 +c1YBUkT4+uRVkzJAWzZ4RiMFeSFTj3Eiksg2J/f50D6ZlpeXw4/MYr+pCmMQOIY4 +0W0RrlF4iNnZ8hR7haWnH/wH/zHNFPwgw7s+WtY0uEmLmDPxxVS/dzzmc8C1Ef/h +g7lSRZ5tdq5oxpyVYEdK1nCSiberwrAT1XyGpn5erxvjeE1kPXro/EXeIY7GDzsA +34FSFBiIaU1Cfn89OOn5M/TFp1+0SYeoaiwF0+x23NBUxbCmAGyyW5t0Pq1PY03T +PA== +=QdHC +-----END PGP PUBLIC KEY BLOCK----- + +pub B341DDB020FCB6AB +uid The Legion of the Bouncy Castle (Maven Repository Artifact Signer) + +sub 315693699F8D102F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEowbDsRBAD2jx/Q2jNuCkgiS3fzIj6EzDP+2kipIKH2LEnpnTiBlds2PFYM +xYibVab/grgQODxTdDnAKifbJA/4h1/T7ba+OV+xIUoSI5MbgaF3USidiDHPX0pY +qvG+k3hKECLysQ2zoZpcC8c2ePiZQSVC2i5BRqgs0xZPz3kiT5U9WPozTwCgtasB +TgHhkOGhZ0SOUuQ4dL54R9cEAIaDjdPcI7LxyOMvvGTuW/SaS9JyP21Kch+Vf6I4 +vKWWqXEaF0So8S088zHnBrcBKhu9D1sKIHS64EoYCrznfMUtoENPe4sf5QuJmZ9D ++fBuFcudQIpkx8L73q+E3fmCK0uX+anqipJtS8mgpMeabKda4KkjDsZkiaNl7OBI +0H09BACofK1HTNHNke2N0wXN1GyG7IAqprKl4lBbu5aRXvfKQ2tDj8s5webNQ+Se +Om/Yg0Bi+CiONLgUjiwYe1wNls8zkk3LwYFeKIJ1AjAY3auBRWOI0/IFFzwTkV8J +YPHa3Dl/kmYp8NMMwA5bgrblggM0Qhnp+k//xpb0FYbmwHMwUrRhVGhlIExlZ2lv +biBvZiB0aGUgQm91bmN5IENhc3RsZSAoTWF2ZW4gUmVwb3NpdG9yeSBBcnRpZmFj +dCBTaWduZXIpIDxiY21hdmVuc3luY0Bib3VuY3ljYXN0bGUub3JnPrkCDQRKMGw7 +EAgA5MMlt89bomqE0TSq63JnPaSeEKsAx6A1KaXaSg0LEI7fMebSQcAdVdAFBo4H +aR+jNNGv5JGTvAObLrqxnn5mU/+qhdTw4WCf17R4ETEKc3iFN3xrpxz2Vew8ZWpw +3PcEgCe27ZN02J6BgtEqhT9v9f0EkAgRHIkcaFCnxme1yPOFN+O0/n1A+59Ar8rm +wcHGopSoZlGDEdEdqElx/shQjqq6Lx3bWYXS+fGzSAip+EAX/dh8S9mZuS6VCWjL +x0Sta1tuouq9PdOz5/4W/z4dF36XbZd1UZHkw7DSAUXYXfwfHPmrBOrLx8L+3nLj +NnF4SSBd14AfOhnBcTQtvLuVMwADBQf8DC9ZhtJqHB/aXsQSrJtmoHbUHuOB3Hd8 +486UbZR+BPnnXQndt3Lm2zaSY3plWM2njxL42kuPVrhddLu4fWmWGhn/djFhUehZ +7hsrQw735eMPhWZQpFnXQBRX98ElZ4VVspszSBhybwlH39iCQBOv/IuR/tykWIxj +PY7RH41EWcSOjJ1LJM2yrk/R+FidUyetedcwUApuDZHnH330Tl/1e+MYpmMzgdUG +pU9vxZJHD9uzEbIxyTd2ky2y3R+n/6EkRt3AU9eI0IY1BqUh0wAuGv/Mq2aSDXXN +YJ/pznXSQBjmy2tvJlqXn+wI1/ujRMHTTFUBySuMyZkC0PwUAAnWMYhJBBgRAgAJ +BQJKMGw7AhsMAAoJELNB3bAg/Larfc0AnAmQbEg9XnLr/t0iUS7+V7FcL5KpAJ9k +3LS5JI97g3GZQ2CHkQwJ3+WcPw== +=DGI6 +-----END PGP PUBLIC KEY BLOCK----- + +pub B6D3AB9BCC641282 +uid Eclipse Platform Project + +sub 700E4F39BC05364B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=uOQ4 +-----END PGP PUBLIC KEY BLOCK----- + +pub B943F5CB616566CD +uid Marcel Schnelle + +sub D73B23600E60600B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGAc6SABEAChVqKPKOBNTaP8H0ERt6nFhvRMycGYxv/hK4rokUBOtr09ourW +VkdKmC81jS7tK/gNAcM0JmwQ7qB7sZFNBFTB7AZipJvTdpNjxPmM120fBouZ67J0 +RlwMs1gydhgovZCjZKO+cNbPFD529/yyjMVB3oczxzLPBWaSp6Fq6bP7nYB2qKnA +sSIDebj4+aeErl8V7Qp3A2bHgBveMZBQuLHFg52T2+rFLcP2GU+i1irBbFGvIB6L +2j0yImzHrpEIhYz9I5s/slAggWzGf/8IdgMb4wKGRLr34cRjFTqYrMHUAJEGlUcv +5QXcVnQ9vWRRnI0pfWyRwFlggdCqCm9ArmYJqeatyhnpfEP/LE8fW82EOsJJ/l5a +O5KBWe7pGxvq2oMEV8J9QJKYlJymcEpeLb3y+7rlZLb2qAOznz2jbVXR5Niuq0/s +DkeVPkS9/VxmwAi03Ti7B+hBQqrXyvPL/g6xSuoHYmTNhRfPFN/oD0csh+Ayxgrp +OpM1FZ0/+ahyt/jbqSPiW895vuuHaA/GQFJP1+7G74L4WsR7ZziMcRnz8pUBzQCA +5yrVcD9+abN2tIAXNftJhQeZdyIxoTe2mty8MJfSi0VKHML+bWpkYLMkCoJ8jngM +pVf3loLKHeJAh+KndiEyrCedNU/nash1Kr/EvXTrfyA/R+q6SCxt7bgIiwARAQAB +tChNYXJjZWwgU2NobmVsbGUgPG1hcmNlbHNjaG5lbGxlQGFvbC5jb20+uQINBGAc +6SABEAC/CnL5cqk5N9NocvTEWue7bOx4YTVnj36+QNcm3WNd9G8Ab0ku0Zr+PNrI +7IhX3A+OAx4GaLNLehp5DncPa8CpSrCf8Lx/Dzd7SSB0gnlM3hSvCGODhkQXjIzj +KUc1OB12GX7UvpgrQvFJckWLtiauBVHe+UBlXtgKL6cBYLnhHoqzbxjirX+Ilp3d +4SAk/4kjuK0J98BHzYpOMtJBHDpt7u2D0CNE7+b6yaw/0aex876O3/fF0ETv2vgs +jcSe04jKKBuFFDIh2g71GFHHo9cw0n756YT4KUvNdhWtuSjxqHWQLvNmz69xCPp6 +D/xealPWvdPShfpwTo8FGut+QtGmPmtMqCbsEM+J9S2tHusN2D4iyTZDMlnuI+1i +GpkP9SAFpM5f2GaBMEdpOJ7qZAyG6WSsw9CDEzti8JUiTklHJeZ9gyNGIB0KetMI +dejWPtntvFdbd+uUH6ZSkTrKb788iYkiNppUK8azP3m0lvsJJMSuUoATBu1urLkd +UZFyJVYVSGjkRNvQk/WhB8Yt7kQtijIyrxB9xg1moY7vwtB3GEq8fY1NDgO0MLCg +b7PnuXI5wqERj7ro+o5yc84ZhOgblujHKMHoKb7hnlbEkfYguWgj2LRshqlPKqts +F7hu+PmsGVfMwFzDP8HUJrKnfvxmupV2sp5zzJyVteMD8JruCwARAQABiQI2BBgB +CAAgFiEEzg4bxEOAlXnYcY9PuUP1y2FlZs0FAmAc6SACGwwACgkQuUP1y2FlZs2b +Ww/9Fg3O7opa/uRnk5oqEA51dZhCzydxgt5zJGqxGuFBH7g09QsqCDgfb+GWT7Vu +TXS11TYeOqN9YO/papeltPsxWuRopnI3imvakkDW3JqQqkyL4O+Q/0Hj0wmVh+Yk +nwl9FEatdPu8q1QdNxxjyBhr0ai640aQWDLp/BWVG2J2bSIATzIK5e+WW1CFJrV4 +fjz7H+gF/HLGWYDJKsdjwgO2pNd/6GWRUkvF4OD+XKUG4MGfqLX5yZa882+Emsss +65OTdu8qLHYNQ1Yv2oxvh4+H0CEWSDWOcv0Egw2Ku8PbmLDHWb3ddRlK7HhTxKRO +VYim+YOvodOEiFYWqrx1jMhH1GC7XWu8LonsHQVxSG+iLL2UNoIeOcD6MeFDXYMA +6P/ZijBkbnu7KUPaNZxOuQbFzmchrAlWaBoJcbMQtoAFZOz0qWs8cFd729RcOvT1 +wGiOaBLmR1xbXcJxhofUCwbma1ode2+ye1DXIIeE7/OVVGURYrVrVbRxrK+M/k8L +3H0318GUweeuj0MZ+chxDFLWIE1eXPkzMF0GgGFKkIJMX2eKp1xyhxj+28qijxe7 +YFVBK8LEuyWUMU+oxIojZEr4Wa28AYvqs32UtRyjT1kbm67mav752UPGTs2AQo9R +vCeonVI7Q1CkIHt8u7eMgzfEkaiPLZlI0l0RpfT4pnNieqg= +=mH9D +-----END PGP PUBLIC KEY BLOCK----- + +pub BAC30622339994C4 +sub FC9BDC25FB378008 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFlMExYBCACmdTDSXPwSJeYbfYvHoDl5C7vx/0+LOTunDGJN38pNQHYQAZnv +Gyoc9ZmChrhLoim7z4ILqmNo8eegknepQ3dGdUij4NVIhR+m+8irayTbsNHvo3UG +9y7eM5tTSjyNYkyk5fAVuT7OhzIzMA+qtc3GRVxNYRKnaHajt+pOSqr+uoDtMG3n +6eAMHCAnhgh5Nd+dCFcNT+syl3zCwolA1wrzGxxOaif+xi5wwXjmF/lAt4PDIuDT +etA2/AqPM4zAC0BtC0iqVgVypjFV3EAexm/g0LNMiG/M/krzwjPq5gf1DY/57jU0 +02FpKd79HmR7bHdc4e2olEf9NlHxfbPXDDsHABEBAAG5AQ0EWUwTFgEIANmMpV3N +K8aLrLgQTyh5++det8C3D3T5tkEdljHOuN31/qdKNge8H6uKH8zXRZsj5pd8adpW +kD4TzIMvzIwzizsGw34O9hf1E2XPoDqvQr39p1sovX3PeDvRJY/7JFNt9DsphVc3 +xWQfNkC7JdMPa6JRiFHd3ynfbQ+wplf4tfaDVn1JXAWp0NSGgMtXfn5i19hHQWjm +RNAKNQLdVn8UczI8XdVM7bS4giDpQMukSyjsjgAo466iRK2+8f8BwIRe1JRvF37B +dnbvTg/dzoi1/E4ukwVJD6YE2LlDwzdGno9KxPlRsuY3nnheVgjbrGJ2XKRJkIk8 +7cMGh41VKw6L4usAEQEAAYkBHwQYAQIACQUCWUwTFgIbDAAKCRC6wwYiM5mUxEiH +CACQViGOHi0BoZ78ZJz6L48YNMx8fSdSv3YJ83Ih1n5DWCJgrDV5S3/edYinkoVI +0Lusy3MdftRg6OWaYOuOTf6MYcddO/mY363jiMByf9Uh3Dqq4sKqVLRnZbAqgD1o +dRoj2NkEQfgEH/H4JRVrxquzAKoWwJh3MhY+kajYJRJyWfc1/Bm3Bj1tcMGlGeIQ +fgWheeMg3kxrxJ9TXPqVi6VVPaPKIU5i8l46S+Wg3uvMs8vC3XzOIvhY6cwguJv9 +UkjZwGDSI952wLqnREMy0gFZ+OAB0qJpYM3nDEekWZP38G80kojnN61tZjRThu9I +i8/b+PwSW+nW3EpQZdLqZtOU +=2H2i +-----END PGP PUBLIC KEY BLOCK----- + +pub BCF4173966770193 +sub C9F04E6E2DC4F7F8 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFKneXIBCACtnX3ZQmPujf6ocvdnhsBheze71DSl34TfebyW2Qt+g9NhMxo4 +DaJy+iFNnsaMwLZRr6k/qf+ISE3A4opWAQlbk+Wb5s6DPPA2cHH6W4GdkxtuJzqt +tFn6YtkFhA15Aahr/vz31NBjUJlBmO4PwvkyxiF/MYP6TQ/AHar4xP1RxSYEPcCi +dIQczQ8nXzya4OqOyTfibeGz/eiHHuwTLHi3Rd2kihQnlRQdhE1rmm8uTyzFe1H+ +P7WW7kQgygW6yxQ3J+DXrG8kG+nbe57ZY1oyv3F/fOBxzn/kuoKHZ3JJEMJmTIrT +Lr1ngCZApgteAynRHk4t/SYZiyoyqZCuBcwHABEBAAG5AQ0EUqd5cgEIAL3PEOzt +IFUibB6FYEkObVhsDbCnHw9yO5MAvAWB60Ohf1J4T9QK63jZ5/CiqcDrw+uab9I+ +Ruz/SgGyFS0UXAkwfTICUdhT5kUzZmGyoj2ul+iFDP9uUdEgSgyXXakrxBbBPzNa +Mx8+GyIXRVFyRTH7+1gWgPQsdN1sGYOgZ+f8TMzAv6sxu0JVzjKXAAbXdiZTyJh9 +d6h1jS5Icilu8vRwn3Qc/ZzstBRk+eLbb69wS9YGoUlzYvDBz+5tiNwvHUriKc6z +VT4Edngcr0mKWTdvD+AsvZffU0XK+vxbgMuRWi/51qb+VgK2gFeAseV6a+D1059u +2+5Pn3h/Fv/vRAEAEQEAAYkBJQQYAQIADwUCUqd5cgIbDAUJEswDAAAKCRC89Bc5 +ZncBk0qgCACdP8kyUZVqfncA2RsQH38NFYhBz5MAEOIhCm7qwPC6XG08CUZfpPXd +UbxZGUliE6vhfj7rZbvUKKHlcHDPobdTJqGof2jt2MfsjJ18JY0exSWuVTmNmqAC +3gsiMfEGIqjQFWonfm0Od4AvduLuc0TPkyfr0gobakgYvhDjk7eQvgC1QfKlk6hH +A/OAFW774qaQsyrANrFevRa7CBQEob6V6N8aV1vNxgl8a6fJzPTNBOOmP0mq7xk6 +nykQuqYTVFyqfXN0p1bbTxHBoW/fvdizi7zMSsuBkWUtfG1wyN70uoEQzSQwqgWC +IaddzrPZPmaZ8CtzzyB7+JdSNItBB2Sp +=wK3Q +-----END PGP PUBLIC KEY BLOCK----- + +pub BDD2A76422470515 +sub 0C77E993AC36C97C +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaDypvxYJKwYBBAHaRw8BAQdAv3OEFRIQWBhSii0M3S9P3eGlZLalGY9smzBQ +C0aiVXW4OARoPKm/EgorBgEEAZdVAQUBAQdA2THBTS3MqZPdTuKmc7QkAvlvwmJa +WEQsXXqkjQdEwD4DAQgHiH4EGBYKACYWIQSu/rh4kM398rwIxKq90qdkIkcFFQUC +aDypvwIbDAUJBaOagAAKCRC90qdkIkcFFfyUAPsGJcpKQP2/sQe+XN69rVZMFXhk +dYg0U9EhpY/7GHDpHQD+J5Uy2s3USLxeyIylXUFWtxqOocB+vZhvH3Yhmjhwmgw= +=zAHg +-----END PGP PUBLIC KEY BLOCK----- + +pub BEDE11EAF1164480 +uid Joe Schmetzer + +sub 4BE257B370130000 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBFv1EEwBDAC61jyEM99KH18hI3zlfuqvGoNjTLIh0wge5vXAH8VxMR0ndOID +HYSBT2+L6OeiqKlyhCgF1km48F/dMzyJdTASkNO1Ni+B2Ric1sBxjsSPufkjl4en +yMOl/FuQOB2myht1fCXhlynmOoiRia5J6xzCsCNVGOVYfSru8vpoT9QKcD1OlwoD +WhfyBx/bXsoRvD1CMjQdalcGxv1aJRWfhRumXQwhMPZlFeARAzeDmWNpglqrMnuG +/VADZXZsbLv8VWaequ4wEWiwTOeA6YYElx648OTSv7NjMM7iyPPPWbbUvkVbA3Em +lLBLlGYZTx2nI0B/322SsREcEDwaBzO53GStIzP1XvaRosM/98/Y9ITwB+Oh7ZwZ +dYmmabxN6F5O3v+TNndEW7wgP0lkbsOWZ6YNmFhvoEtd1RxZiSNov5CxokYUrug1 +cS+/vsa9oIecUwxYOG2D1v/pwYhQnr3qasYz4nEEBWHnnkhyr1BbUSuen7w2SiK+ +64cQn6V9aeZYi6cAEQEAAbQfSm9lIFNjaG1ldHplciA8am9lQGV4dWJlcm8uY29t +PrkBjQRb9RBMAQwA7UCAsQ8KxX8nYO4Sy2pzlh9W5FMPwGluuokPA2A6g2Fz3vF6 +2RqeaE4HrRQMpijQCsN3JTJVwDid41X84XCMItkdAxMjmn5zeF/yCcRuHe2Ci/+a +e5BzrBaKE/VWRAkaZSZWJ1MoDdpSxJhLHNFnVrwTkM/SeSNUBk9ZDEC+43b0hcie +fX9bFlc6XPHgV+yr5ohhwcNcrZ/gbAhhN3/xIVmvKoibmb+ZIajhiCP1OOH+GpZA +PT93w9qZWq3+2gvP4ZZ7bO+8N8Gmz24GL3/0eYI6aMUMwWGjy5J+iRiFjb6E+Iv/ +zToyZFWm2VOuOUqy5t4u+Vyk5bl0hATpJICmKa5OFtQwG5Uvfztk6rujjat90xv8 +yzsBvoEUqKqzIzjHdN36qop5hLMnBljdLdFY+Rk9CHdF7MW8Nf0YWbP/3uUk19ut +GW686Lolt8gvBQc4B5N7VtNoXFCKM/I3ufgnHQvDlf8pgdJOcyx/a90V/DpUI1AN +lwg6IsmFZXbBQw7tABEBAAGJAbwEGAEKACYWIQTjqflQeehM4gH3z2C+3hHq8RZE +gAUCW/UQTAIbDAUJA8JnAAAKCRC+3hHq8RZEgEy+C/4lsgrKCmq2Nc7eTdN1AxwM +kj28XQFmkqO8orfJm1hAtVK1KRizkX52RNeRN6QX3pX9s1e3DjJi3Hpa1UWqeicP +A0kKTi2ytUlxR/iZDkaQkLyCCZtWnGHr/eRBdOjblprl5O+v/tcyrmQGC04TqOnt +Mumuk7JNjZ0QAVkZUxdmfi9bHaF5W5vlcaFYT5gdWpkOQ0YaWXXw5ynh6Ookjhq0 +g4pZNjl2rdWWyTC59YIvC9THx0+vuyN7xnSWIb8J1IjEEYvPqRfpd8s1Vf2AA0JR +PjUG2UV8MZqu8k8x4iC2gbdji/vyg/ycdlRT/ULyNprz1nTLMfhBT0Wmy8B5lFVm +e3URmld8T90RPln6Dy+c+IKb/79z3FPujuSbipXzx3QvGwVYyP80JFn7CJluOl/u +8vxi2EVFN6aVqdzwoswFE3+0W0AfbpHUUT4oeBW5OBTJ5i1Qb0DT6WXk3Y2j1Z08 +xxhY1RITnc2C33wjXAW0h+qq7/7Yq3w3/7ncv9sWIzU= +=3nku +-----END PGP PUBLIC KEY BLOCK----- + +pub BF984B4145EA13F7 +sub 84761D363E7B0FC4 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF7rgogBCADU9OwoEFdIgN5U0JU5pI7s3T1T1LeDMzAQ8l2Hq4jFrhnrjcEA +ieDSut1YIv5NTBoZv4CrklaKvvQNUXPvKrFImA4OKhBodKV3wsq2efCATDGa1JAw +VEJx6nJxxMsCLCJvmZsD+YE8/DIBI6jjnjh8jagZVkxkSRPvUIxlZCxytIyqXI0t +O8pLh8+8p5e0PgGb9OoszxEQZdBavsixdpe+0feU9cz0l0jJYx3W4ErZeCGGwNat +UUiW0ctb3iz7BkNhhoV9zepxkSLzCf5zBeyA+WfD34028pAfPpyAfDYXF4x55sVP +/3MdWGB6eU6KzPG2/QV/6or5E+C1yCMrnMy1ABEBAAG5AQ0EXuuCiAEIAMExiS4A +u6FPSlMyjCaT8EfxP05ey79rYSSZd/ixmqyVzJkXYbf/SJCBeX9+NtWU3LEuL0L9 +WXgAA9Ys18NaJzBMC4kiQJfyXK6Kak7sUfZWWvx3Ad3It35X4svJNuR9GoqgsOvC +D1SPgK7MfTcAF8e4j2UUqgRnjM5S6dRa6AH2s3bCj8GE+YSH9Ag2osNcKBgosNiv +w04tYh+sjx8W5ehKaOEMrBDO8OMYVeiIEOvBIjsCZpJgAnOBPh/7zIQ72tPa9Ou3 +nAWBwdiPBgqv5FTIVmHWMfAJKRRCW4ri2hN6toHGrjJMgobtwW56vTibb82EGLdY +3BF41DQT2MEiM1UAEQEAAYkBPAQYAQgAJhYhBB0Ki153xninxyREWr+YS0FF6hP3 +BQJe64KIAhsMBQkDwmcAAAoJEL+YS0FF6hP3VWoH/1Uhih+Q/iJIddvBatWdzpgO +e03ppK9pCWZ2KepukILbR8bpX3cqUiTbFD3W+ybrrY0k4Oe9hXcm3re0N2GAfEWf +tRknxXH0TMeyWoBlldfSM5DjrljM88XAIkk/T2wcARv1PC59IIZGKOpixItF5Pps +YII4YzlripU98sXBTSlJLU1/UZaT7XNOZ9O1/PVxADpVIeH6MVdWh4W7AV/dYZ5j +d31NbXDTtyDJBUYoiq2hu10+RNoqq28WmJQGD8aqIuKOpeBA8EirLcRoDGELSqYT +lQyC8nl8P5PgkEZ5CHcGymZlpzihR3ECrPJTk39Sb7D3SxCW4WrChV3kVfmLgvc= +=WqT9 +-----END PGP PUBLIC KEY BLOCK----- + +pub C020E96222A31FB3 +uid Eric Li + +sub 55CDD67958ACCA47 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGAephsBDADH0j84tkTcmvOYskQWjA3M8hLNJI5QdagcYviR2yDTBq7paSP6 +hLDrcCwTfvCNIatYI4hGau31RlkNKJHZumMZzF8OarWuKKkwwik3Z8pulMHgC8kR +SX5QM8k4wIeZLbD0UzW22gr0oLDilb3cFr86Y9T4AD6Ke0JATFRU+TrMAT5e5iYe +iVcNGuRQMvjncJobNIt4AeP12GV14p0GhAlka8Hwq24dTue5xwBJ9GwYWwPz6dbo +k83dZJdhLDfvL6ojG4umByeqCn/ARuJCuI0YABLO7BoqsAJvMfCMciP6Upu4iVT8 +DduCd7mV7YaByqtktRJDzaNiJa36riYOnzAVsKB1QbnWD2tP2kcR0N37104+WtkH +GYkSfnZujfvmoHf4hws+6oUgfPs+1vMMYj0AnlotcDVez8sHSAwQN+rzfqqii8lj +9DdpScq+yamQratHBHIkdyx1xyd+Xy00Vs9NY03gIeFFM2Rjat+XDfK+uO/Mpkkx +Gcf9d6lC+OGUO88AEQEAAbQbRXJpYyBMaSA8ZXJpY0Bzd2lmdHplci5uZXQ+uQGN +BGAephsBDACvxELzqfLQrmLHOlpJru6cCqQgPCE6DIQnNYJ6nTyAow6tBLQ7b2MP +ACn1yqRskE1qzh415B2tcZqN8IpguN9NssqINyKdxcOYogmcdfnhN8TWYYUCKBD9 +DssMhz24bq2GjcLxyPagrvACI5O+k+LYE3TQbLE/t/6oG3grgkWHJWKLA/ou812+ +eDOI+/HaME/1uT3DwsGv57zoZaIwADWmdotoEyU/d+cK+5A2727PCS0hfDleDJ1T +rJoT2YRN9YJhGTl3xq/XmhfmcYX3KlTKakENZXsi/x9n2sxpCpE1xMa1kIB6SmZi +l88oxa7+zunRVNK3ymGxVxnLlltTyO/VyxQh1AJFhI6n35ls3l4BSEmUjNRi76VX +cuFH0TnyKEIHxZYv1K5FjTF84yukKrLbWKgWsvr8exHQuKjz+iU5NvmhY0g2CLwa +9P2fjA+nGWuKhLZyDh8M0+sXEuVTbiHgq8dkr94yBwqCs8quqC4PHW4bSivZj5ml +nrgujB35H/EAEQEAAYkBtgQYAQgAIBYhBNvV4c9Kf/PYynRZ2MAg6WIiox+zBQJg +HqYbAhsMAAoJEMAg6WIiox+zi/UL/1/OT875lTWbpoPIIi63ymL/dpkinRZQMQWY +jEsMd6Ea2/tVCbYt6zFXZNBIbJ3WmPN1/ZWFh+PWHla/GlUhksPuSFt8Jf3YL0QQ +0vHwErdulWBLssWMGmlQmISeRVYPkjha1gpBcbKCaWHhXRuf/FsrYpb0NqkArRf1 ++fdhRdsOdy9avioy5l+/Ld+puaJWMKJbet2ARzQ9lOWCyOK6JoxO8U11jupKOMfa +gp5iowztQHcZ53IvIFJGPDCe0pb8l5owFpG8rmOrLuPVlRTMvR/n+MnI3hkswg+4 +2Y9hslJKenF2utD0q0eU6VYnsquSHbsypDjx6zwUZvaa6olCIxNZoVJw/wSv1ZDe +/8U0TEL7OXe9jA6QLEDYAPytSF/mVIqSp4dgAPrADXSt8UOvq3jQoNMTESbJWWX8 +169pc1yMD6HBdGShunnJ+slCQ/nJ6zFSMKLTJgOp3pRJYxfDlWoZVm5mSLsif8yD +yc0PGiCz66h9jPMKXJJ1o9MGQKvydw== +=+Ahq +-----END PGP PUBLIC KEY BLOCK----- + +pub C21CE653B639E41A +sub 4F80368F9034B8D0 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFTkKGgBCACwO2VP6PNnxLBACsAcrBR6Aja+KRUtrY0SZZzW/AdQmSm8a1z4 +aBF7dud1bzw7voZo5ieGK923wUB+R9vQYd5DYfNLBHj9/TrTVCfKfUIeeEQRZYBz +ufYcDwi4uVx9VPj2wRhkK+lzxphvosJCNFK8Vn82oY7eHQ1RA4AEhCeE/hz8maq6 +NPoOPjpEN0DVnPIYdjPsdqd4UKQzkX/wMOxghz8SdcVROzUoL+9pZzx968OFuGrV +lUD0su37S6To1IUn6WNEuy1uJTzT3Zqi0hfm31AqPxlLWDOwnuKvUJl3RObyli2k +CBtDa5xPE6GU6ZUEFUZ7qbk7iV5p8uTchKVbABEBAAG5AQ0EVOQoaAEIAMQuOKxG +Y4JyXr7683S+bwlagtViPXGey/A8Bf6b5uzW0SguRoF4oLpdfFBE/NKuBN1iItLU +zBvqVVjvtb9HRLv0Xgf5foXO+osCYzpcEokNY76uv6tDYZ3tP9JIUmDVsndAMCdl +WZLXA/d3SVeBKk5tTgpNYIoAHfxjecLJUJDEk/51GFBpuHAP0WVUgPD0hWQ2dgsR +8kch4GjkDQVJlhtoHxMVK6yQNuoiqMJDamV1noJ82MJX/GUROjGTNCTyzlMP/q+M +myPJ8vv1OebNrJtF2QnCbzXWuTd0qGgvzoBlmRcdbPVJLDezJr4c52klDHmnlRWx +gt57vXbEIVfUcTMAEQEAAYkBHwQYAQIACQUCVOQoaAIbDAAKCRDCHOZTtjnkGnUs +B/9O8dX6GSpBURjJY8ApqHyhkqmfX7urnjiFen8B464ZX5EsijfvesW44cCL1oSn +Uplj/wLFKkQw/5kx8GTHZYJnV5BhL7wriQy+RZUM5fTlnnqZX1YS8rMenc4/xvAF +F5XxCBgb2/EOeneXSjKSHeB+L7fmCM0vMf68vkE0wyQDrtXFSX8KF9+MjHGfiB08 +5ctlyouM0ONQ8n64GJQqHVp2tdEbQXT2WONSX+MUns19quymQV10YMlTWLt3Q7jc +4SQCeo8u3jHG+rqyup+93M+bmT5liG6eHAHnGQdAXzI/z1t+3StKGHVii8Wg8Kcb +yFe0+OWOuDHdhQgvnZwtl4e3 +=DwNF +-----END PGP PUBLIC KEY BLOCK----- + +pub C3BAB45F4AF71FAB +uid Yang Song + +sub 34FEB51E33761BEA +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFkeN88BCAC4rvR3Dc6nDYhbXUC5IQ6SJWvV98+tvZ117J/VD07el7dicryY +H3OAWl62iLjHJFP/+AEra1plpiWbPioDlzjOWC2AJjUCtqVLHdyVbY0Gv3sSRZXJ +3H7CyGO1QbT79m4Gwk9vX5si3aBTzJNqYF6Kn1C386ZSZd6l618YSPUzJRXi1mst +rP2iV1hP+kydcFxB0+F8/IjzK9kngtp/k1z2x0tbIRTnUYnvS/GNbLHlVzrxvA1S +izbVLBzeuzmSyqT7xy6dPjsLEIoSK3cyR9x5tzmcSEA3dMN36N7j+mbqVI25Md0p +gd3vgIqGXQsRSezG1Kj5pQglPKl2Jufx5CEPABEBAAG0HVlhbmcgU29uZyA8c29u +Z3lhQGdvb2dsZS5jb20+uQENBFkeN88BCAChDNcKrhc2DHhIBmGvPvua+JiR3bAD +4pQZbMfisr70ZFHox7mVxTGGK7UDY4TpgeaWElWY0FJuQmnfP90Z4z+0Aqn24OoI +XyddFzt5JF1jCT79QX95t0KBhmPgqQssCNHfz6tpQSgK3NRaVJSjtgaTaVu6zJ++ +ZJj772LQt0hqzcjMlpOaX2Jkdj8hDhsw3PZiCO4Hq52pIS987sPQRdbT+Tv6V4WO +Z7cuWqQIdfWOQwxVn3MAIIL3l9a4iMKIAWuj3kHxaHZ3gLj/51b/nlh3chDzhGbo +YpZWk1pmlCXFZnctoRO9Tk25bKRUgTJJP8sKgAsNK37n8zGAzUc3yXhRABEBAAGJ +AR8EGAECAAkFAlkeN88CGwwACgkQw7q0X0r3H6tJqgf/ZXG41Egf1gHv2d2c4hYI +xFFKhVJFZI0EfMq7ZCPZXrjphtgTrLLUYyxaRoW8dRstf/sNyb+yiZr8PGyKmk+6 +LutzHStJFyk+h9b/sRKWzKnAnUzxzMggfFChMsMdwrmd4nmy6II+A0bjxM0eee4A +vEyd9/q8LNswtqaVJf7Az2HADvPUxxqcrtFvUFvD1GuTNYaSEKsZcr/EAmP8zq6L +vXKCmVnnIiP5JwBYMOfBqnsG6r504vpfmhfJBycCsyZlJbVjV0sL9514Ph7MJysI +uwCK4i39LRwL4i3O2PDZgv5Oal/V0cfrEEfJD4px/gfmZF2LRkpMS+qZrRJ2Nu7K +6g== +=jgaB +-----END PGP PUBLIC KEY BLOCK----- + +pub C488A74FCAE540C6 +uid Michael Evans + +sub 49B78FA5343E54BC +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFKjSJsBCADTYM1uFVPehqzc4VVOjUfeKP6x+ZRoFa3nteA5i6JykajXvauO +OkfTLDDVE56G5mDouvlWaFMDojcRBKjokHBAGlMC8bhAx21JcIIzELvGeWVqilS+ +AV2w9JluAKEQ1xVb6U/poITb3mqsxheTuaoqmY4wm4e6154gFOSjFzZqNFhpNK8/ +E/hrrpeBNdTW0oClQdvc2D/hN4E6ZRqYWsuQD3yQdBArP+qDsdrRl4AaX+mMFL2c +3iSlUX7RZmkk8RwuyTmnbLWPddLXsjEWRp0haSTOsI2D87iPIebzcuU0dbEsKh+c +HRlStSrAnOxB5Jw7dDOUm3J5SLjpKv4MNNIlABEBAAG0KU1pY2hhZWwgRXZhbnMg +PG1pY2hhZWxjZXZhbnMxMEBnbWFpbC5jb20+uQENBFKjSJsBCACV5WaxMM77npaO +8Y/YY4W2DR43rA83qutQV5j59s1e0GE+ehbJFD1edL8Te2w8buBqpUy0rwg1n0BH +F/tYV4K73Vcw3otT5fryeWanhiEiDY2U/5U6pKAl1QWpn/MHq+XClUj/O0nyH6Rj +R8jG2VpCcDx+QgScdkMSnscsVRm//BQVbkikgiEKsZgzzHNl8XXZBhiRSpmGj2w1 +oPDh3AwzcjIxg9V6Z2BJOvTBaZqFK+amB3DLja7r3v5r5zMggTo6okyeBzkAnoDc +f8yX+hURgs2fgoba1X0aDt12y56N09w+Gie1UBYB4iFDdSBuEz2kCopSbq6NCise +4QeOK1MLABEBAAGJAR8EGAECAAkFAlKjSJsCGwwACgkQxIinT8rlQMYlnAf/XfbE +yrLCPbePlUVHC529t3NoGiXQhlFg32TCRj0nnET3Zn+Umdre/OMEjDXuNH18cmys +6qBoQ9S5POtZMCArRRiW0qjWT7zmCCjh+4R643Zoz4PzFDDoyi4EWp5n3c22b8Is +KFShQ3EuePxTIPo83aEFaG8UtAh99Rfs3dztPDldJmWkuku9DtjEiwkwSHZJCudS +Z7rHLWQgXvqR+BzMdtTOrnSczwUmWzZz27jvwB+a9khipinBJ2JzaTr7UK+Qpa30 +c+TLfgWL/sZd9di5WQjNCD0TRnM9zEtLZBNLoNAJXqciagZbu9lTdhasxjaUq73M +5TAfYyOYCcMsXvmdpQ== +=l6GR +-----END PGP PUBLIC KEY BLOCK----- + +pub C4C8CB73B1435348 +sub EA2A558279B36E6B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +4q5nL0cFWrpYBtEUxDwWSI3K +=qAlt +-----END PGP PUBLIC KEY BLOCK----- + +pub C51E6CBC7FF46F0B +uid Ron Shapiro + +sub 4006CBA6D352F1FC +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFbgSbABCADGGENSy3oWLjW7zfYMSsR0pm3l3eMA7ptyU5C0U/MoIYjbXwyX +XtlGwKnNgngATh1SMpX4WDbD8tn6vdeP4uHQsDb40t0XN7/HISFcLhV5pCgz2wNR +t1dZbreV/EaZn3f84Vnm9s6TU7RHuPP9qBne1YLbB+LKKZUfQGavqGR+n322DhrR +NqLdC3zFNtSmCVXS6GZ17IV/B+6CiNdbmPVk0o7zZexqZCNMXxv1IFzHV1/Y8Htu +WQBfZ0zWwrX85ZKysU7UDdoyUw2aa8/+YpZmKwo32N8GpqA6azBeUjjVXGuqktMy +l6IcHbLlgymkkjK7TyAKltuhvp6AlBTzp9i/ABEBAAG0I1JvbiBTaGFwaXJvIDxy +b25zaGFwaXJvQGdvb2dsZS5jb20+uQENBFbgSbABCACxPdPzyrpxDrtgfkBhlfXt +IyTJVvq7NwVejSPWWkcNFL8tMTfo6oTLMgswfD9Y6E2SlLJivabWj5TLiq02kP9B +mD4unD+RGpqYcr4yvJYy4YULogMbe88L+/L1+whqkVBHALM7CvBTwQFxEbEoreut +a46eAIu9XMkIPGDlw44YrLB1pp3hfEyd8iLaHI+CsQyi8aGPoReuHqW5IyJKX62d ++Il/M3WTEQjSZYfqEx3v8bpWC0A24JIEGvmOoJyplGYtEBlNNZOnlsP1N8jguvfv +noYmHGWEtHVqTxPx+dUYs5+0IqRXhX07ImvY5Bo8zMihxo7uulEUPKJHeBOnr9zf +ABEBAAGJAR8EGAECAAkFAlbgSbACGwwACgkQxR5svH/0bwtXNAgAsuPW+B+mc2O9 +QKPSR0UaUGdlUlZPusT25a9QzqMrQwv84VSnpHqxOzgRLpdFDqs1MizcJA4h8vuT +0Yumarma/OT5WlVeY+g5Lmf9JXC96H7L7UItQ8Hdav/Uc3nUlI42Nzopj1g5AwSB +sgV6IccdQcUSAL5UZ5PWoGqX2wHcLb+nI2MVtEw3D8eF1vN/ZzDdwjBFrZSNRoSh +mIuZ7FMO93LaUqvXZt6L/OcpBTqgf47r03D6IjsDxPuqNDgk2aOtFcD1H7+7qF7Z +Ky1SdAdOpnyolIGb2qqaWA4vSK4XB2qzs/DA9jq1Et8+K4k7C1RD8GPl3dOkMnpj +Mpx6qXA6Zg== +=o5FW +-----END PGP PUBLIC KEY BLOCK----- + +pub C6FC46EB51CF569C +sub 92BF193AD552C6F4 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFwrYN4BCAComkN3DymSPvTeAV7G/vBEYFKEANL49MeQttSZRzHmGAIAF9Iq +fUN9Iwp2Ii40sdUV1+03SVnprf5M2xdxO42AB1qgHdKcyQnbUM9oV/i+iwSWZOjU +23HTSaKkEKgVGfQUQQbXibQiANTJpRmKtOtWQsz+zfyaiGKJEKEa8KZsm6guibp/ +2omOJ6xWrZNszFtQsq3fsdhTVbWTHTpaSMV8Wf4Ue/g11rMrR2CPqzMFrt2sOVVy +Np7c6tzdDeLYzCDfS/4QS3aGRuyxnt7uNhH11hzm+IvJS16Jg2oVK4lKH5FLh/zl +myX249Tb+ht1i+daMEn0zpPqiYlNtCTLPLMHABEBAAG5AQ0EXCtg3gEIAM48wHoZ +BNcXlO/bQTFel8yN7vxzcq8HvAr6iuJf7YOx0rmbFZ2AgJ4hqiJxwx0kGyrgaJaB +xseQWnkOmm5/A4ivM5RPAu2/NQX+7enFUCS7zSFg6aSOVHJvcs8oLKrAYko1kODW ++lz2f91p9IbRDteKzfyK7w6A/oV8zNrOpilS9esVroUbQWLPf/lU+SKQnhmp+/mf +xSDRf0N4+7ebeNOTX6yyvhz+Fvu9OaH7CqgTsSk9k9muXeCCXNfhkq1/vdx8aFFm +3L7ZLQ0mjYwh0Fz/M3wG0t6U1fsOkD7O+9GSoaDzuF1iK8OlvhI0/bJC9tYlAdB1 +D1ag+vHCAXqDThMAEQEAAYkBPAQYAQgAJhYhBMpJFwTWE3gNK+4A8sb8RutRz1ac +BQJcK2DeAhsMBQkDwmcAAAoJEMb8RutRz1ac3K4H/RMOjjOC4ReAJrmvJOAEBSu0 +6b2NjBgAQIPCS7w30ygXCMzyRsSg6dFfTeRQGndJ5/cytZN4Q5wMhL0yowa4DDrq +vhaWLqImSE5J7IEaQznm2pauDZFaqQH5WAQ8jor1YlZ3KmV+s2SBbjCVZs062TGr +loY+gPEiijE04UjHGURoLNwE+aLmJykYwcaaYdYJ+VkgoPwUaYOqenjVTmjncY7s +iEiqFENgBAVsdTzXikt6JoIZJqKuvopirIOpisWdQZoEMYEQ0ASAjHWbTaNm21gN +pPi/dSpPzi5np24la3bu3OdZ6EgrPfP2wFRW9D1ZB46nQIwhEzJkDcBFn2JHCnU= +=+FyM +-----END PGP PUBLIC KEY BLOCK----- + +pub C727D053C4481CF5 +sub 29E792953D515FC5 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBF8pVB0BDADcwRGpJUDe8eVSlJ0yPQl/CyeYc0RWq2f1seUMQO0xFW1xPIeL +IE68D9VdgarA88qDLYesfBqzn57/r/ztj2aLEKt8IRunJzd0w0G2rrgSCZQ8RmzL +b6qNocE4EqOluhuzHBI+1+tqoZfVTkfhqKQw0RjP6gHPrelYPuxmzXX3dbpf9eam +yDdr1tztvI8iIwYvHoy2HNmkXMUJwlzKsRrU/x3SMnEqTIFqGDy32zQ9QdnMtVbd +lCc9IWnleospZN52+jeXoMhRJYc/pSHSMu7DSo+rHs9a4NxHfaPnuOsw/Sn0450R ++XbBV/4oeWuJ7g+MXvLepB1LTC8tETWwYFPyA+qmFhbFnir+i7JKEEYO3TJJgkLG +xlXlM7DAhHIky+jokeNl1n2QhSSuQ1dq9KCLlrpDrSGXaBvtonPyBT/Ik9YGgR4/ +ARctQLaQ5AucEPHuZZl/d71499y+IP//ui6SOH3LBIs1nqFmTGXMoEGHbIyEZvjk ++wLnG2YrlcpLtl8AEQEAAbkBjQRfKVQdAQwA06Zqf4RRCqPtmx4nqG8wXLUayoWq +4hIQpkajt7UYBejFrScJZeE7Oo8HwiQzPs6iBMUQQeZBn8gQU2/C+ZvTD9WjhRFq +I0CXcCS2VL7nciR4yMwKrrlf9LJAhBjKfw+07VEF8D/xDTcYuGXFIuDtEu3Ncq/t +8C6ybjVOFGtNFRsGABiMm1PKIA3496f6GQLo3oioU5jizCrgouk26Ak2hK98i3+u +tOjVYBIcQ2Y+tUxV6AucSAU1zLVqVj+SY/kVaj7hp5w2sGVn+4r5dsGIUG2K/VjI +PXOfmpMtsuOLBN2z80RtO/b5OjHJCftjI9KqnyAozw6LpbNkmOhRnfAPaslfxUyx +iHbRg+RJ4eA+4ZnbAZPzNvBt9TLervLhYeh42d4XGA/uicCCwMyFUxztOQ+oWpY4 +fR3qhUoGNKmEetBcJtK5z+LQipd9GuRKpylJBG9eU7ecimrmI5pMLnNjJNkyvd5V +DCoO8WbX0AO8MV1s/08sduAnHul9W1u+bkADABEBAAGJAbwEGAEKACYWIQTbBZfj +FENCJWvIHj7HJ9BTxEgc9QUCXylUHQIbDAUJA8JnAAAKCRDHJ9BTxEgc9Se7C/45 +UwrGhyd7CU4dMNgpBW0a+7cFcbjfyjSNOgPDbn4P5B3G/o3sTOf8K8O3nMgMnQ0F +H7TeLwTxc0vF7r26jG6E6YCZU19yMef5xEJa08YqUNJkJ+2Fopu4WhS0mEbpII5o +9fMQ93urEEj38hCV8lUyPLUPQpWrmyvS0Pta2Y9xDpYKy+bKQLMlRGPna8MVNXWF +Ud4AaOY+us8/rbRTWURB2YuWyPyetXw8veIWLJgEmbRijkZbUVroUJ70OXB46dOE +VktUJ03N9zX2SpiIJ2Lg4OAdi8tdp5EklrT8Sp1vGZOcwA7r/BbpWesIqbHNftRM +jxPuWZ0ROY4m7xzhexeOAn0HQNg6INswfXKuAJJNv+DEe9nTxpi/TkO8Ol5MYDIL +vKyZs36osPk0L6EEgVzrqH9zUKTiq8toczTtb0uW8f9Mru1UDtme13f1tnq+z94Z +cR19NEV8Rfs96NyYYUvfC+ro4to0pXe2n4X3BixeaBmNdHWd9YYgXjYxnM4pLyU= +=5vxW +-----END PGP PUBLIC KEY BLOCK----- + +pub C7CA19B7B620D787 +uid Stephen Connolly + +sub 7679164AA2590985 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBErg1IARBACVbmwMwp4p0ldolUYSkGl7XFJHwtEWmuikGcM4lp72h/YhAXpf +RVsKE3aCy6HSTt7KJrcUuOL8BB67riZXLOIZtA9kDyC+0EUbnW2EbVfJXskPLP5X +VA7RqcuvaW7lil+Fi+eWsy4dvRS0/guG7SBlMpyoOOcuSK1fGlMJkRKMEwCgopOy +9220jSQTIvq/2nzLR0PN1o8EAIImG36FVZw8j1WMaCMfJMX8gZuYxdnBXo3AgU2H +x+AnWvyMFlxR7fIIOsYaMmGLcIY0Re9qQCpPaZum9d9IRvqH6YGcI7s00ysk+C5K +09FjQEtcgAe9WBVOSuHorcv4UMnX6fyDWHucu63KXrwqfq6YCHHV3OPTs6CkzgNY +iomxA/93OVuaEXHkyPBS75br0rl2/m7Ow/qp9rvNOzS9U2ttJRctM7ts7TwtYLi1 +YHxHeTgbnq5lVCdUnY8h4hElGx/hi/lpyCJEEK076o/M0qTvq9JnA1p2dKj07lJ6 +ffANb0BKwV9zhvI9e0+hheROQiHpLM8BhXHgptSeAB2L5rjWkbQmU3RlcGhlbiBD +b25ub2xseSA8c3RlcGhlbmNAYXBhY2hlLm9yZz65BA0ESuDUgBAQAMJcjsRrUhLv +q0vxeuDhSL61Hz1LtU47Q0npuY3xyKCjBPc57UA8sUsXMzPdMBgeX58RVlrhUEHc +i29mkt6ZpKEW7QyK91kxTAX6Ic4xFLMS48uudV6JFYe79cyRaam/dvlTnh1euhsE +SkFhwk8ovORv+puEqQxBTjqKWIjt2mGLNMijZ1MoKGUFF9A9GBjTnvJEvC87ot1V +8ABQjVjeyYf/230Ap2kdwlBk1jJ1BOjVzGOpG/H5eWgTGs94LijHa/gZEXeXLku4 +tqNpE/I7Mfz9WoGr5BQcmDu4zETm7JDK/il/+5cLzYJwUua+H0VcwLCTD/TjEPmt +aOnUMD27Kpn9MMxiUx2PG0qFmuo+otx4l634c0huG4EQFqkEIEVQyri8Os4NJRRI +kewjNxjMjV0c/45ki5dHvzVYV29Uq2MOIz0oh8RX6v603SEzWJ7sBVF0J1pjgJEa +jRohFTSH4YkequnSBb2G4VAzZi01YLJhk97KQZ9NRWCO4zurk406TGqOBmKKf0A6 +z2qVGgRs2MXyOi+YpGUNxUUMFHU7kBweRS5l6k5D4posE6RohYXs/CJowLxN9r2Y +giLEbpBhPJyRa0amiTC/X1sJ5DMwNAgIhMcskJAys88ZGKOGMicRQ8gHdgJOed5b +JkwKY0A/r98jzCVKj2yl4/sscdGmulaHAAMFD/0RN53mDdp8tBIQWU12PRWlwPx0 +fIMqHc0MQFncKQxGUUC+j1uq6gCoqNOXXKlXA9c3kL1iUTF+2n9wkFalnUpYhUhL ++r5SIDudFYwwnaCxfhrehMdhGc0wYxUqHLE3zx4qnJwMJ73Lo2u6/+w8xfIyKehu +rSIQxnhiSU8gZOuerHqksvwNFjxURbGw5OydXl/vBeciYAFUUOmV0+/bWZb6D+8w +qzESDuxpjuOCfP5MbGBBE1IBjElPo1VZqcKgdRW9VYMIHqttiVChU4hH8gmNpN4L +eAeHnuAgchGD95HmtkwhmRnr6sbPg6QQ+zM3NIq4BK5hGsTXWjzHPnw+7e1MbIYs +Oo1SkFe6LKNfgMfS4CoZRVHOV+FN2AWFh9p9Gbk0b6Zp9S+Jz30ZBerAJAlgE7x+ +AqzwVn651fyU6OpqjXJnDLbk2SAsOdtcqWG3TzTTsS6P/9KoO7ESGcjhdCUj4rxd +DdHR5CRrOF8Wflyjwc5SXq98Q54jKn0HbYbTLidJEpocFS0Sh8DUviiZMQ94MW8o +IHqGRSw8ert/P0yQAvvgdRF1+EcZ8/rs7zMvJHTe6SRyoND39V/CzwuAU2BgYbJH +X0hXPBxWF1c9QcvE9H+MdnU8oIfcVo/D32gR4Fdre4IuVKiBW7QhQLo9RkyEkh7u +wDkgbmvLFwh5el1o3YhJBBgRAgAJBQJK4NSAAhsMAAoJEMfKGbe2INeHLcAAn0o+ +lXUb2Mc5eHXkbkTBofl4tYxdAJ9EO0kGWS8rPdGU/dAyHRCoHM4/FIhJBBgRAgAJ +BQJK4NSAAhsMAAoJEMfKGbe2INeHLcAAoKCttVyQMou1JLwo/NFNrSiMyAlcAKCT +wRo6LpahA0hXIR/dwnekHH7Faw== +=sl+a +-----END PGP PUBLIC KEY BLOCK----- + +pub C92C5FEC70161C62 +sub 64863FF4D1BF1809 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEdUhrIRBADCU9cuKc92CWQlZxwtRuSIV/36Qmj264YD+Lix+r1Qe1PqRr1I +/MObOo83ulorWigSkx1k81Mnr56NwmIeo2bMhjmgRgf7EG6XEbKdRKfJcJRR1lDV +Ml4ru40W958M3PX5fsi0m0g2TuVrAKIS4vscUt4L/Cf4IT2/0OhaT6bWswCgsNws +Qq6NtCkLkpWSBNYGT4zb6yUEALlhHMnfzPSDerKjDOaYHTz3PRc/GGUDSBTSVj6W +hQIDrgTqrPxoB5JMnfUz8BLSayk0d6HiwspJ4Wnxe2/jdIT+6xhX9xBYXVHZVs4R +cr8zbBNcW2kwFg5Mqy7TiAPzakzCslKAAX+cjAKSOWyRbmkEYnNgMlctdyENOR9+ +BpP+A/9anoVEfULqoETShmgWdi94gx713qymhNBpFZnPpm4j4JuxKopl/unQmw5i +Jwtu93cg38UfaOMJjTi6tJ6F6SE8xXv43nKs3Xb+Ll1MpTgaGUXEhCOeTZl223Qe +NBUp8kvfcys6aVX6GT93dmWxtMewlc6gc7HVQnUnyCFsVeoy/7kCDQRHVIayEAgA +/OzW6erYExaWTjI7wPnD1uv1Leq2WRhG1I5YfuKU7K91TMilBm8L+qCmF1QEg7yF +6mYtdwXjOiA0YoGOVEeNJELhJFKZOoeZob+R3DC05uUsBl7xi3NgB8Msags5N4q+ +nqZSMZaEDl5JR4ZAhYCZBy5xBmnvmRPUL50CDN2IBKxHVOaUllBIZDtdtVH37Gwa +VzXuhPxsLiAOeJ29W1t8RrIP9TjQlPhzwu7P9Fq3/JcCmhF8xOmcn3wfCZ/VMteF +Vp8aTr4aO4uo0O/HYisbStUw2YDGe/RmXiNOD8CXHFOg0/c4tettRhtnl8OO3hQ4 +srY5eymBG4tnV02l3/Y2CwADBggA2i5UGKqWDJ46LviS1rNzBLLHPv7GASFicQY4 +HxMTvREdBIdb+p287azp0l0ixaLQOq6HgSMZbexRG/DdDSakxlOr+kil5NJnHmZ9 +tXzGmnLP1WoxQEc2FVdG/jKTg6gz2x9Cz1pRMxxAHN6Os+c7hxYKbD649fBbGPgZ +PP0OCjwrHVfu3WaMaek41QxnFfk5s+YNENly+XfeX2PuYLwKwuVkYJftqohU9bRx +0phdDgQWbIZMVzihxr5yTxfkCvmHlrLHn+lAOz3N3xh0Qh/DofWEDAee8uk+pbzC +XSON2v4iO9lsHg+wXYLREBHxdE0EreZu2VzBFa9iN2nhtJnuTIhgBBgRAgAJBQJH +VIayAhsMACEJEMksX+xwFhxiFiEEGQ1alX/yInPmAfenySxf7HAWHGJygwCfTUtT +D6aQF9AkWwwt0KnH445Fg84An1hG98Cj9efdOoxMt6lAEHX1eHLq +=ZGKN +-----END PGP PUBLIC KEY BLOCK----- + +pub CA80D1F0EB6CA4BA +uid Sylwester Lachiewicz + +sub 5EAB8AD72016DF52 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=AKpr +-----END PGP PUBLIC KEY BLOCK----- + +pub CB43338E060CF9FA +uid Evgeny Mandrikov (CODE SIGNING KEY) + +sub C59D5D06CF8D0E01 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=a1yG +-----END PGP PUBLIC KEY BLOCK----- + +pub CE8B1D1D2530EDC5 +sub 7ECBD740FF06AEB5 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=R7zg +-----END PGP PUBLIC KEY BLOCK----- + +pub D041CAD2E452550F +uid Deanna + +sub 5199F3DAE89C332D +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGCtdhoBDADdopjDt4eUNEqLJSw1ZICSR0oq09SOVtJSaSYdF8UiXjBfL1Ds +fhTDqSv5pT2a2gLj0OU3tFhWHvINLaKKCjQnHVcFXi2LTxt+XBOjRYkFjHVisbaZ +PZ6HnTMStPrvs+hQ168vU3VfYOsOLN22j53I/Ba+FA7E0G0bqkratuT5L7BTR1mC +fqDaeisWSCllfe6EEysaFF+/1RcRy+Yt+8ZWV0FZEF7UwQvqKHcYmlkqPIn3v/8y +J/yvmzIEtCQ1F+bvJbzaROmeJf254G2Uh7IfMYEm9WlqnGwNdbIhil7bdxq8Y/0H +XbQPaESxkki7yL5JTfH/+UzdklMe+Dga273L/cgzfjV3zJJ9vR94W5ABAbGYh4ZW +aKvNnT1m4vTbEMfo4r3NF2zc+K9Ly/JNaHqkR5M4SVElvN2lsC5KNUiRvExhg+h0 +mKyx61mu3gUIrC1UOmqhtx7RzQQf7ESMdzmNHY0P93lR0Ic10fyli0wfl7A6q7+q +zV2a1V2k9Yg6B9sAEQEAAbQgRGVhbm5hIDxkZWFubmFnYXJjaWFAZ29vZ2xlLmNv +bT65AY0EYK12GgEMAMgP3//QeBsTS3IrfSp3m44el96X6BWona2yo4DvVyuwqfUL +ZE+Nhj7I+kEZLrA29AOySOD/6quJ4MIJZfq/Do920Di8/10WQ00OdCM1wH7bMz2U +vcSqsr0iOgQtycuUf7JOHSTME9vqk+C3Lhn0r59AVaRdXEe6zBgNZyzZJeCr5F8w +RhglPlwvhOGs2aLEqlCxFnY4pLayQFoQyw1lDjHIXHg5JtfOHvqiNXVDcGpyKLG8 +SzImp62iL4sfuA0weVIQeS9kZiQabSYKvSf3TvNXYTgmFz/vjPbYhv9LTkBroTlV +g3l+UmAxLrHVuXMx0zX3jfNNHAqUjVhPYZhnifMkmGJgLeMIVqr5Q/tx8pzyYiiO +cqQ1zDg8ubJDGRue1JjlUGdw19OvhFDs+lydukt8Mmhb0gPkBLi2syZHgYHtEooX +PLwEsJ+SynZCFhZiWj8BsWNFJpaDd8ynNeWhMAcwi3B5ZeQiZaAlV0sItxsrzvbu +4ZYZtkjAkQdsaaTWSwARAQABiQG8BBgBCgAmFiEEaWthmaKp2MKc54zA0EHK0uRS +VQ8FAmCtdhoCGwwFCQPCZwAACgkQ0EHK0uRSVQ+G7wwAvaVPDgnM+i2pGQPwq6Mk +SzhKEG4H1pvBWyYR8H9D3p/dE33IjVu3EEy1h37Nzdyp46KtASGNe3KBodSsh6gv +PlV5pNGxMNbX6fo8ZGtS83C+6uTF1cYmuO1nmi8P4+7qtcNZg4xv/ujAZIC20kem +YKDth3FvPxEXsoxY+Ns7sxgd3SqoyLhjcyoczI8uyhim5nfvvbnEd6WrdiBPBtb/ +F1h/nfqdFj2TcZkAlnzGnlVlgU8J60u6zE+9VvBm0lJR73Ar55mQEwarGFPL1a3/ +A7ZEeNa0Dc3Oa5sKMYtxMlGKZ0WGUoGcDWiaDEsv5YyRnaSOaXKM1NkJCR013QAr +RcHrRBPo+0/RIZVE+b8oEcmGzdL8HNwnm7e06ruZryF9LQA5YBmCKE0urigmgEvC +zZsj/fMJ+OIZcAhE7UVae48GpW2kLATxmK01oSzvizIlmN3rVz2EnjOun2iuuEpF +/lmDbjK5n1r3f8npB1l1fT5cozzQJkPVYzhBWH1KXP5X +=nh9O +-----END PGP PUBLIC KEY BLOCK----- + +pub D1031D14464180E0 +uid Viktor Klang + +sub 326BA6BD408BFA94 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=0Zos +-----END PGP PUBLIC KEY BLOCK----- + +pub D2151178A123C97F +uid Kengo TODA + +sub A66A7ADB0B51FECF +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGfeJOIBDADF0P+wY8d6xbPIOps8ond3glh4u9PXuL3o9TiyC6zopFCwc//L +GjPqKEgseyEzjgyTwU8HJORt+N0Kohn6N2ln8GTv1neqVf2mBMgRucvFLcRSFhgU +ILtJ3r5DLHrITu6JFJwiMyVq1fZQg4XWAAyn9ctAcxW+pjC8x4WmyNsUPUHZN4/P +WEK3kmawx6y4i8+1yH1ebzy16GRVBD5DfAnDzJumyah/HFvr4gXvvUKybIo4pAts +6i2Hffg5A8GAGBRTrKH6Oj47fbVO/2ojy1rj3gq5sFvB7qsFtdgXS2NelOa6mYEa +bsYEH9T13Rsk8Si7OQ83psOG39zXR/2FxgenfO6yrRCX3/2edTDE8YV9Tm0oPRaj +Qlhpx5IUHH9scEqgntG5PPzuT0kG3nsyDkmvensOLOtGgST/VuJduKfSjQbTRJwJ +YCoQqwVAMGQR8zBieCUqDrnIZO8ZvJ866YNfdh6SJPELx2+JSBFJpdNXSl0Mn8QD +UYqQ2eehX1FACo0AEQEAAbQgS2VuZ28gVE9EQSA8c2t5cGVuY2lsQGdtYWlsLmNv +bT65AY0EZ94k4gEMAMUh5TL/MKjm+WAQhyPWk7YeoE0sAUopa19PjpXFfc4Lxo+w +3yzj5f436+3UBEUjlZLS29LnotuaVzm1/aKQ0X/5Dcb2263NxrcMwChaoUWdNoih +gQT8M04BdEA32wPfC0q+IOwb4ifjd/z2ynhs7T/zLpgx7bXSnvRltWQLFjBsyL5T +EusUBwG32srLZ+tGUsi2laF4Al024JIX+5n7OPTHB3lFNd5lSmH/OtXV+Le0LxM4 +R87JgzRpSmizW97ZdLWMcUD+mcEsX+UVvxpO5gV+rbWeo8Ikzwmd9L/kJNfmOSi1 +H22O4xVQQYjSl8DpnO/SS6oXGTe5tcfRD3hWBwwbCmom5kh4Qp4J02mpsV8/Sbeh +P+Vyx9zyN86HvVEOaR+HhIZrAvCkltY22fhfWO/fJgWGFb9xAaLKKki3VAM63L76 +IuTMh9GWOG8yDeazO6ZMuMOKy+N1frnnfhtTxpMa4aIzWLcTXMkNQ5Na4gRgFSU5 +lpwIu6BtDqVCOnVvYQARAQABiQG8BBgBCAAmFiEE2HjUqCPq5zG3KdGo0hUReKEj +yX8FAmfeJOICGwwFCQPCZwAACgkQ0hUReKEjyX+VXwwAv/tNnVZKu4FJu3PKZvog +oh1gJIRvf3PQ9JYdXT9TjChUSgKM/hrzmXBJaYeWLlvcsgutNBxGU+QyvMdmhTjs +fC8I7R8yzJhe8G/fH55qvsvHH84W+ha2IV9Wlo+KnhHXpsIqt8HDGfUADQpREJ+5 +JoVVqvrzpuucEk86w7//olSl68AhH1E/ANsv1Jgc5YeQTMycoGV/ojYe1h2VokCg +I9eS6YCUSJSPW3uRK2zI1Mnt4CUVjZOhaXOyqN1VbFIWwsBs+pnWJ/brNEtjlBWi +L9P66ikq5XtgWD21uwPXacNDrhzKuc3Snsyt344vApQcpuNnMdCY8rlQ54wlpRt6 +OZ0GW4tpzlWxDzDdDWUqYZ8fXUWqLZBqNdfF/faiKWcYLVLxSv+AjXrYBzjsiZRz +k0pIRi3YiAbICiv3wjqUGt1R8SCO2F8XzDKT8bHhPUQlqf5nujd5xb81cD0SU8tw +NINnVCD3nc/mh3Vr0dbJRKF6QoekO3w3iQ7YhpdNkX7f +=+han +-----END PGP PUBLIC KEY BLOCK----- + +pub D364ABAA39A47320 +sub 3F606403DCA455C8 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=5tlI +-----END PGP PUBLIC KEY BLOCK----- + +pub D46C5610D06E7001 +sub 00E25FE3776F21F2 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZsO4fRYJKwYBBAHaRw8BAQdAcxaiyqFbaECpSz7mhsXzopzN9Cxwv80WlWGN +gM3qpOi4OARmw7h9EgorBgEEAZdVAQUBAQdA45tqjMbTvozvroU+Z0nApYG0r3Zl +gI27fUSEIetPuEIDAQgHiH4EGBYKACYWIQTlLweHelgF+a9KsKzUbFYQ0G5wAQUC +ZsO4fQIbDAUJBaOagAAKCRDUbFYQ0G5wAZG1AP4m+dkpV5g3AT7Lws/lUDrKrdTr +5noqEjUmwUNCiuTOugEArO87llEEIabZngdpe7D+dvJ7Bb+BSX2fzHKmrsE6uwY= +=9gxs +-----END PGP PUBLIC KEY BLOCK----- + +pub D4DA5EAB3CD7E958 +uid Jiaxiang Chen + +sub D826E3935EE9DC71 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=Jp6+ +-----END PGP PUBLIC KEY BLOCK----- + +pub D9B2BC72A3754BEA +uid Lukasz Piliszczuk + +sub AB0102C07B1F0046 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFU+j6MBCADGoZExS/1Tw1yCTCJN9LMh9kMW1g4NiEmGjRFW1iXTQggbIx6E +3mBnxoCQDwatdrFOV/FzonfEDoWsJ/vwek86ygzp4Ps3bD2mdysxvbPDaa8gGZvg +4MaV2rDZabBpByTlzIdWoPfeNc0MRjBYErJZ6fxjG59IwvPTNXCSevpBSD9Z7CWt +SjVMiZSxEWM1uE6a9qEzKOSmGskSzTKdPvNKCNKLE5a4S1u+D+ZIYT6ZzphhUKrz +LQjYa5unANFSCFIKMLZOSfU9bN0JbzZiO1t/WKxiN8V76o2XFXQO1dl8zy40Daf2 +9ncodzot2zh7ohiwyLVW5huKcJb2jzKMUjozABEBAAG0KUx1a2FzeiBQaWxpc3pj +enVrIDxsdWthc3oucGlsaUBnbWFpbC5jb20+uQENBFU+j6MBCACvbrax3c6w07Lv +jeJPO0WDRcm2LZC2XmzqYkUeQbqfZD4NaUNoADPrCk7OypUlLbYWQBADHZY11L2K +pZkRtpkhB9VtuStyRLFGNAmYRdou8eFSiMAxCiQWhOCJOqv7ZK7q9UfhhfR8K3fA +9y9RP8581Dz5mWTcXcz7Al/Gl1VFQT4d1g/UswGquTEtbN9ssydztvcFI7Kw+il/ +e/SxzH+Y5YkChCnVV/H4UM3CWyp9gly1gAOMVkQofJKilZgJ0eIK8YlF1hJyshGw +/yAPTyS2UI5R173qHdmH35Haxr6YYH8TdjI48j4HxEOfMBJpzyxBjMy1zxH9jTI4 +KrkjGAW5ABEBAAGJAR8EGAEKAAkFAlU+j6MCGwwACgkQ2bK8cqN1S+q5ZAgAgSUQ +i3hlvEFSttxFThsZRuLFJW3sIW973QkKg3NJ8UjXQzvMHNUAn8pYKtRIySsU8iq4 +301Sibi4MnNePsL5/iLDL6F370RFcLHJqk4345eYPGUeEx/WtlKXio+rUGc2Gru4 +MKkgsXyQoV+A7GIWgADRVPlgN/qqKNt1DPffNNZL9+41T5/jL/8VhlxNuDcgEuoK +FJYhobshCLPsYJCkmzvdvKIqm9QTk9cW3Erf2zTjQC58obLTNoJbJzrd5eMdP4Di +vORFNTZUXz3l4ozi0GNR90DC2OjvB4uFQwtHMdvdHt77HEDJt6+5DAfezNybsnpt +QGQE1hG68gs4NMhImg== +=re7c +-----END PGP PUBLIC KEY BLOCK----- + +pub D9C565AA72BA2FDD +sub 9121AD263441EEDD +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBFrjUQUBDADTMQL/4d9EyVhsO4XBH9wbGWxcEJvsu/HvppN5fY8hpMV0+Cr9 +wjAeJ7d9zdFJVB8vPLN7bb5dm6SNyK3KiOugqVgZrQ+ZPTvCCgFbFyEXuZwDiOa1 +9oMwKypq+GyAqXnfNkQTx8+7PAKslPoEKeft6g7T2+hb73nf2vpnOfpp5ljQhWPe +YEO2kXIikCxVXK5uSpuq1JLjLB+AFsnERCEGqOCueQgrLyPZnGrk6i3pEyz8b6Mh +8NdFjztqBWUta+e26Z00CKEpmGYzoV3sHD8Bhf8aHPWUHp9lGIAW3klLbsp1+FVM +20eF+a/f7XK9YBzd2dqIYWELdYUB+XU0EPFTgYAsXG1Z34ObgOVjU5gjDSNYfvz7 +tPPngDB7k9w7n2NatF3aHpHvkgekaLlflmZ5rQnMadhBUWgJoiwsx2q4TnSh70/t +TI3dPBbdVG+8YQ/LpNzOVshY2uMHHxJq6lUGVl6BIIy83Yslu0gFYHzL7H2tXKpg +Q0qAQBktmX6H/P0AEQEAAbkBjQRa41EFAQwAquSR3AR/8vClCEfG5TjqL9+eHEIF ++MfnheM58ONv+ZW96S6WBfYALTzLNM0BYcIChRwq/EsWWPp2IvYdaVC6miyV3jS2 +YqWqFLn8lwC8lyaI7wX+ee1JY5b2TOCep9MRzFTOTbh8RpJDaGcjK6g6rGq27C2N ++bLIs3Pn8F6uHT5shp+YQksMHNWfw16hWnSvI0naO5JVccLZwZThZUqkN4k9KPaa +QeYdd+coJBmN730rKq8VxNgoNWO70AXa4Qt7ddnSAmQM6iP7H2C+RI8NQ2GNh+HO +b21jyI+ZkUDQrFJKLReAbfyADhzedJT6aAyzA60wJwab05lP77CzVn2dOpzCY/hZ +uM2faz33YEZ03RapYeYhRIUYpXT0nR03dc8NbRVtUqYFSKOPab0CI5UmnML82v6G +o8N6eRTiSXHGwGEbr3pCLwTFUJTYdku0xGA9TCnXTjUsNW7+Cco6bTUmtEZOGomR +GBhI0qG9Uj73jWXNZLgLvfFYj27Sjb/spVARABEBAAGJAbYEGAEKACAWIQSwIzWq +VMzyHlK7+avZxWWqcrov3QUCWuNRBQIbDAAKCRDZxWWqcrov3fslDACSodXt2F9p +haTM9aArlcNszxMLSb0ixO6ufdm9PS8kzlziot3lZrxVcEDEkqM9CUHyDaAf4ZB3 +geb81NGrYDdrFEwNYVaxK19ay3aYxKyfup+r1V32XKxTF348JsboZ72wKig0OQpt +ZnspDq14BkSIJR1SQbOCkJwnHUv29Lsro7NxN4QUddYwLoOHlFwO51jlSO1ngsYy +5QOfoAszn4kV9zY5eJAUF/npCHMTvPACZn/zZUo0ycxAt7f6NE3OnTfmWMaTk9Hi +AgucaxI9lmk6BN3FOOeUqt2VslIMb4k1pX7FRRsvzNuWTJO9S8DPlpg4WjzWtu/c +HYCSq6h3mwojlt+h7SA6Ctl1uA+umidvPp7ozubbxsGdCCdxGSVg4zX/L02i+5fE +zU7kkPUpKBx6hHg/zJnwTVAY/g4+Iw6CHwBhw+2/KoMpjQ63VqjwQZ6+VIwdsSCh +4wzAJkf+DhvDmH/G9sIawu3JcVj1Pza0Iw3gLf/9xZrbnl+HUEcs+F4= +=Uwqi +-----END PGP PUBLIC KEY BLOCK----- + +pub E08F28E25BD93E9F +uid Jens Nyman + +sub D7B501D37A3AE550 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGQdnjEBDADm35hD/kMiNXVocImrrdOkA4xvNQ8OLirzETb5DSSocIW+OBLZ +V3k5OiKTqYxlZIa9JT8XLLVc8pi9Is/x/xjoodEYSsj0LL0BODMeyt6ECmfi0brN +CqKv1J46cuWun0iSog3KaGrMx1tcNAVvYHdDuBlZ8X1BbaFwSqdNv1Hq9LkfOUwB +kafs8vXc+5THSkB2XAq9v4B+P9Rl4fwMPsjgkS0X/3JNMq0dqlUXuxNTj9h/dNAv +l7sYEBF5gY1wqmVbesHWurl8Bqbq6wvDualZ0ccGyisaaz2FxIKuQHBK63ffxXVF +/AABfDBsS1nx83qK3T7LFemSwQ+hMdKjck4Vng7RbB/FlUNZlbTiyrwZP9492YG7 +k5EcbB1K9tIPJDccYN3zkmYbaEAHL8j7PnTq7XyRPrNYFLKWBBa3sYz21+d8Ym2+ +ExuFgqvtnyToeKVfGX+mNbftrVK4j2OL6fZMmkDL/6niG34UJvHdsKvqVddTMPXM +1jlZYh35WbM1eIEAEQEAAbQeSmVucyBOeW1hbiA8am55bWFuQGdvb2dsZS5jb20+ +uQGNBGQdnjEBDADQUQl5FP5g6/KTI0aAe05jJk8rkLK7nMQK7ayHdij5RmuRPJO6 +snlEPmSpd80BL9sCuCq8Fo87Al6NcD8Fd39IZl/n7ZbKZHfcSQNkyKlGoNMprAtA +nh4rFdhG8Fn8Z+HRP5VcwG9BUIzvScyCLF4f4gGaaOAMd57I4q1+PsgvUZbGee74 +wqfLBspnkut0CEblr//nYf+Vkoo6EfSwi9QbJLYacy/3IrfByUIyUyrwEGFWMpul +RXFzTRExpnX2j9g+XZO12LHMxJQ2ptOI70m4JDGd8MnPnvMofLsy65DBz/5qZKtV +8haE73KKSWjJi9d81kwAzZR9WjltDDhyYOYYpDFcK4x0Py/XB5Unj1jf49PrgF16 +NVlfRh7vS6VeqUoNHYAR5qagftBiDZQ317GpRpm3LUidqyiFexlLhYHGdUk/vpAO +SCK6azGBmW+dXx/BCC9fX6wVVRv0LNeAWgnyf13SG0d0jFJjzxqRzXclsffo/oEd +JV7MQqo+ZHcdI8sAEQEAAYkBvAQYAQoAJhYhBC7okAcK6TGMOtNSDOCPKOJb2T6f +BQJkHZ4xAhsMBQkDwmcAAAoJEOCPKOJb2T6fNkoMAIVtequTvVXw9Ax92uPbmCDg +iNB2aiF4U23e5ZTPvSMWZlMx8hgI2vMCgRGcNOcEQcIyvFy1dZ5qxqzd/QjFvaY5 +2JutEUbeXvNfKmzjn2VzDiW/SHAc8KnWlAmJfqY+SPSIJNJ72Qc0EkxuGbjbWW+y +eazFuDOafej5Cw/nRTMvAVm5hElNKtdRztQVJOOBPEFefbb9YG9gcVVo0Jxs2WY4 +1rh+GjK8rB/PnK4NicQcbh1hk21enla6j2jYKNhCJkifw0HXsa185cnISSv6fVsG +H8tCCDoaZ8mXhU0W49n+NyWhRdHM9Gr9USkYVsHzJTZgTk429CI+ThEMjsxeIpJH +Qu/m64DNlDu0rrzHpgjyUedNB05QEwEtxv3nMKryLcYifLhbrk4ktLIMtYLdDgJe +ddn3I+ytL779X+SOCm292NQdg9+M6hkvgITfIoPe/seHjc0hP5COIbrfDeIdciyJ +1c3wN0nS/z6/50wBTdmj11Kx3Phb0uKtekcn3BVXug== +=36gl +-----END PGP PUBLIC KEY BLOCK----- + +pub E16AB52D79FD224F +sub 5A34A5E06B936F93 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFF/4bYBCADTeOLZiVGNbjlPrwG7UcMl+yXmEqpf9dB1A9cuicH3PWXj0WOb +LSzHjzoRvRekEqSUmgoveey1lPuA2qjOUkXY6Kiyx+oLiG0/ObJHUQW2O+tjSQ0R +ZXKd4ftaw65SLbwYO2JHzj5fLC9j2mZQiRjGs1bWM58c/dOKp1XaOc1/ffcl3L3q +Up64jWH9r3yhPemh5SHo47UxNvItdaJJYnt20azpZj9oq1ebUuQFMaQDc/RTALhf +Xb4BWO+z2PCmChz60i/Ko2ZKPJV2TqPqWO+aklgxTTwZZ0IvgFm/5n3Dtn5p5iGf +qwKkHPJIDWc8cWYtxC608LFdqiAlYmp/oPi5ABEBAAG5AQ0EUX/htgEIALToF36j +45OitNd4k17BSZJKnuS3uIL3tTw0fRqLv0/3EBaj4zD5Qc5YTKFgM66Bb5ybI63c +wYhfSBHP2ZRS7oNdDbPd/30jDKNvmcDjIhGLT7bZJwC9SJVifHuvtzr6wBR8xoIt +yYva5D3ax8ZvnzqIbMPeHou+0ZnRYSPjy2c2TxAJTjDOG461h9mVXDdK74wL8kQs +IxqqYRIeEdmrXMrd/B8IPwuIv8w7LwzadNgRnXaJ5Q5bnMvvhVLnWKRt5aiQVBxc +67FTujjqFF4Y/1UJb311K+1LSqNrTT7As8nhf2Gu/Gb47kw1bb7wBdKv2Swx5mYq +iW5+ARQU7jCiUVkAEQEAAYkBHwQYAQIACQUCUX/htgIbDAAKCRDharUtef0iT2Sy +CADAznSkG/8EdIU5UQhp/lY9h3WLzYI7aARw0IA6O4ijGLwcytO7TaWjEzUCMZdw +01vAjVH1xNn9QvTgQV+2GyqyBNsjmgGt5/tK/+JtMgXUwr8+KsBf3908rOqAAZ3Y +GyM9N8sRsyfPB/PHfv289sL2IKPxiFTGI0NGS3qOAKQ5TZvV7OPsP5+yHfeJG/Xh +CW8p+nkMGpH4rE8Z6NKgLe/WC6J36aQ4kBfYneueH90Dc400rfGyL+0Gn1Rzuj2K +FuUFK6q/GBlFaNo0azCqtdpcO6C3GpJYtISxpQ1Rp9kSEzSCL3tOli8Xs6gsruc+ +vCSIy8lzRw19ZO9G7qhjcHLc +=vO0K +-----END PGP PUBLIC KEY BLOCK----- + +pub E3822B59020A349D +uid Kotlin Libraries Release + +sub 60EB70DDAAC2EC21 +sub 3D5839A2262CBBFB +sub 9351716690874F25 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=7RJ0 +-----END PGP PUBLIC KEY BLOCK----- + +pub E6039456D5BBD4F8 +uid FuseSource (CODE SIGNING KEY) + +sub 4697DFC8F2696A57 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +3i1fFPTLkDPpOUpVEh9FB0MrCNxY+0pa +=iicM +-----END PGP PUBLIC KEY BLOCK----- + +pub E93671C7272B7B3F +uid Rolf Lear (JDOM) (Used to sign JDOM Packages) + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFCPD00BCAC4tY8wMQTsCKyII/mMkUDAkXA2cLM47fY1Wn+iohtgtalUdA0v +AhGvTdFU6/St35rOKNoyLC7Sy30FBYpAEfMB/x9j/CaQtdtGhaQU0hCvtWGhhS3J +BJb3BIzhKuP2mx+6tgSPtP/meiF/K1GV8x4s+4JyrVxSFtqz+yp5szFNbIXA46cg +UWOkzPhwA567Jf/8dUWysXb3lp59DG9anp2BQENwh/EmUhuhlPoM3PeUIPcZAiyD +SMJCR/KooLzE78lAOK14oD8eV0qg3tT0R5RlsNKfFRRoo2bwwPhz0SUVqJlt7ehd +By6ztFKej4M4ZKonnWUiVJhKEGm3dVoEAJTZABEBAAG0PVJvbGYgTGVhciAoSkRP +TSkgKFVzZWQgdG8gc2lnbiBKRE9NIFBhY2thZ2VzKSA8amRvbUB0dWlzLm5ldD4= +=Q+9k +-----END PGP PUBLIC KEY BLOCK----- + +pub E96EF7AE7D967845 +uid Niklas Baudy + +sub 24219A5BAF207160 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +N8xI5kHJ/ihT12hebEP+EzZPnA== +=WExy +-----END PGP PUBLIC KEY BLOCK----- + +pub EB095DA7D2F6AC0E +uid TensorFlow Authors + +sub 603D72C90616CD6B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGBm/LQBDADMr/VPcTvE6k3wEYxq5kZusnDCDTsI6RK51d4oMwaRc3Z0jtZ0 +CfyWocZBok4rMbZAVnE3Q8pMyikGGUnc8ZsWoPEmJyCpw/2Orj0QqZhIgYMQ31Uq +tiGZF/G4w9phLIkFgU9BLGYjRNM69R0oE/Tj8mjguvnKzYM3GjkY6nDsgWCM5TJX +01k4sdLs0dVg86m4keq+SeS9uEwnINZTh6kQUKsW6aHvvPXze/UPoaZqxgXDjF+F +JrPwW8yDllkbzpbo53ulz1TL5RsIH3daUwxXG3ciovUXG/b2ZRuWjtH7Gn/AvCNL +0RXdHK3A2I23zCooOE+we2D45QUHm/vcmvsnbxOU7Tslm2DsnYxBjf5dAl2yZn+J +FSrV91Bbd5ZXi1UkkGjBzAgbHDwdMvL9K6fTO9NwjXyBpiHy6ukIOObn9uIgDSSa +xPnqgeykSv+RZEea8ML4BSif5RJYlmILEzJhH4rtX9X+t8BZv+ZoaN6p/qYg4/2+ +XfSUPmCJjlUaIUUAEQEAAbQ7VGVuc29yRmxvdyBBdXRob3JzIDx0ZW5zb3JmbG93 +LXNvbmF0eXBlLWF1dGhvcnNAZ29vZ2xlLmNvbT65AY0EYGb8tAEMALbsrC+JdcC4 +wyIL2v/pg++nHpMUbueLO82DpIf/OzSGerT5D5cP8PpuwhKIbq8drEEj6wpSaGjK +sZT4EQppo+SUFRAu/faZLyWW/kMLJQ+ez/OC2J7Yqqa/p5pker4A5WcWokWujlVJ +ZVE2MA6R3EPDKHF2p3uIgHmvfWnMS+tMzopj3mj1SvbV2UNZyj1SFrOkUmJ2ueBR +WZ9Ll3xuEMfMRAF2HXFI+aBIeCRbRHD/ueWFVqKX4vsyEMMCeyWjumWYR3KODqQs +E2rgf4V9klXUoo1d9jvSIR1zt8TyIvuUQv4sB64Rd5HsTS5X1VjJ2/0Vgu0TkF3V +BCuGEBi8u/nRXe5SkOYG1KAIbqljpx8mkgMjZRRrptxUeaK6GndHikuY85Np+FDD +zQwKKO8hItXHQq8pAyXm9RkA+rnEMfqKi5HNThsKlYyVNxQ0I/8r0gjyprLfZUhk +tyR/ZemnNN9w29qgD5FMZ4cNsTeCx5NiKKL5+UmbtSFMx/P4vh4aXwARAQABiQG2 +BBgBCgAgFiEEdEDx99J7rBFflT1V6wldp9L2rA4FAmBm/LQCGwwACgkQ6wldp9L2 +rA5EgQv/dXw1tFelPKUcFuhxa7gJ6v9LkNoCEq5g5aSc7IEjuh0EH4aNU/+7rmIZ +wRslOHcSBeBvQPhIYdosIQfdCcwjS7LU1urnkljCvuIbwaX8d5Am7NqMbtM/GR66 +MRPSO3uV6g0DmswDZ3i0WSeUgK4NXRRmL3gUZcoxOeQpg0Mo+fGiZDGywACa3azJ +BWR8G3gSjrN6YdM5+Tb0gQ1D/AN8JVF7ksknl2AlCI9pOEOsyaHZ4T++or36btHm +Z+FJw9LCPGHaCTrDSoA0Tun0fZBBIf1xmNPNgdQHerTtp3mtTCFRdBiIH4d3s3d2 +HBuEYqdLqiD6+8saU5lRAPMFb6VoPPsGNv8by7h7WQ+fWyuCpUPZvuFuVobq7K3V +H0iNhLlzae8WMDUVX3pQKZrDjlqjjaloOJXoq8ZFlDM+1wwKwz7nouEJPFReP9Mn +xjlUofFR0KyGrs6qzaeC22AvbIotX6sUExVRjz4b+lro3f3lLlHlEWWyoKRyH54g +1y4mk1Mn +=iteQ +-----END PGP PUBLIC KEY BLOCK----- + +pub EB380DC13C39F675 +uid Mark Vedder + +sub BB09D73166EEF1AD +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=V/CK +-----END PGP PUBLIC KEY BLOCK----- + +pub EE92349AD86DE446 +uid Thomas Ball + +sub E68665C8F91BDE69 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBGO91akBCADDDpIrW/IohUSJNDu9VOUlnfEOm5VS49uqM0uucLi0BeAhy1Fo +P6Yg1cJkcK66DtnUoTM/JJLyDzJRlKnniLrYCkw8ScvtPdA5cQKJTY5ecn+9ouR2 +SC9GkBMgagbCScP1xE45q5FO+z4kwmcERIKOQ687VAk64QM6hJCupfAd6SqS/X0Q +SGttTNtmj7YBpfnU5iFX05Hj8Zkk7CX439xltO8uJNyBlDVbuUZc3/kRowKPVuuo +TK2mzllVPzE/YT6NUY04wQPmRJx0uWZQUyDBZeckdurpSImdd7sik6Wf6zVGvxvg +MC4oMufZ3EM8R4dssRSIUfnBaQ2o1LS+GVxjABEBAAG0HlRob21hcyBCYWxsIDx0 +YmFsbEBnb29nbGUuY29tPrkBDQRjvdWpAQgA0k8hiP0izEoo5Ys6Ra/ECD2j2Ssi +SiEExo4ZGzHhHFDICs4JShob+qwbdpU6LzAwvyFArqqhcAMnoUEbFDrsbyml6vNF +KPU7cEkgsHoV3VfzDjRLqFK9QBmz4MzUUy4PiZ/rJ27JlRUiCEZETE15Jv+9+fgp +U+p1IAPu5Z5CIhRi95pevCi3z5Ty1E2C/4tCkKOGbmG9bIi7HGkj6Tk3TtX3LTJE +ZaIFuCp2Z8dXB0RtRIzSwxhQlRkpb6LB/m1mKD0tm7ccB/+/JeMFYbrOSSPA7yic +oUu6EviOTKAcZJrAkfBhGC2WSdRnQMxVzjnVtuG4UcTR1yBAJ0BgmwjrowARAQAB +iQE8BBgBCAAmFiEE6xs95xcTyewuh8wm7pI0mtht5EYFAmO91akCGwwFCQPCZwAA +CgkQ7pI0mtht5EY0LQf/UkEplnz4ksZwZ59yGwRbFw1E3zIG0O0MzykSwL30+36/ +MtoBhgVs9UqnOvnvMjFm+2byZRtY5rhnx4e3MtFAQWKsq693jthQ87Ie1915xyvR +7uJBDaulGY5WueQq7gscj5/iMUSb4977cLIth7+0Eop4E4qNEBalCLsnPUZBS670 +kW7h9SmHPPTPkvJsYdrVPtBn7Z7DySouXwQR5kCrjH9u6VtpfTnS9+yBQufVf0AQ +JbQJf85p+8W5KsRmwtRLDQrObOow84sxc6H8RmyinXsOqEZ7k07sagKezdfGqOmC +AwT14IRCih4oeUsICRfb9e23BVJGWs2x1VvzaIrLsQ== +=vv+C +-----END PGP PUBLIC KEY BLOCK----- + +pub EE9E7DC9D92FC896 +sub 3B7272A25F20140F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE/oyDcBCACgYsHtmWmtUzqyr/JN+orfJaTl2363qiS+NJ1lt2CNxUWOqldc +VcIGyjmzokxTRpGdCFmT1Lh/hzZhcDPLjrtxf+f6njIibt80OiEbX39gjwZRIikd +Uv44Z7zAGE/upTM87/s+1+M1h9NFHPUg4SOOHwk3hYvCbvde3bZaUDhhn2asrYb2 +wlUbPBZROtFWlxZXHGXsMnER6ZScxK3ckrJkikM5L8tGFrJlBb/lG2vS00lcZDoR +7v7XtmHqOBxPFf4RIMP/HimGl4lEuBhc25eAt4QxoVmoqb2C4vCUWxwfAURkJzI2 +JU3l5YP0rSgO9rwrXGlTvASCuEoQyPDjwKGLABEBAAG5AQ0ET+jINwEIANpeqMgb +VzzRnT60rHPxCM40VfwVTYFEspNZV4g9Byb/Cu4UqHtdOApqIS0XQDgT+042x4Jt +I6OZ7fUzOpN6Xb673LnB746J9cOlDL9JkqUwD/sjFg9YYFMROvhOOB023moPDsPk +9jSDDxWmMfpr0NWGba4xWG/R6FHAbK023oRZO1BACBcNF59M9y6iJ3Edb6qfiGXF +fnJrvwOYfGEqzOA+5YJZHPI59RoxAMm7NT1EwhgnD3qw5qCBHBDzaI7qinAy5Zs2 +jtZIEZHlV37CAKjeXjmjCnAnYD/Tlh9vOY9ku3UK5uH5CGxYDKdd5bX5bNmWK2mb +pRk7C6MSTGX9tTUAEQEAAYkBHwQYAQIACQUCT+jINwIbDAAKCRDunn3J2S/IluSv +B/4o0x3rrIK3T9WK2Ylvh6eRLbB18fEiYj5B/aWRTYTHJRfGa+Tziwm55stRnQjZ +ZMC36K7LjPQjqjAW0011azO8oFKFbJnVmmOTUrosVdkxYBdW6fcOXmFcFi2c/M4U +At6Mb9qg7vVW1DUk5+W5OU3TC33WCBKClb3usf8Uv0hZ5TRs+gt35EWBYpHuQIhq +W4+1ntttT9gF6+MruoAKy8ViEsPQ/tpLG0eOTc2XjWg9z2wVl0KVKJN32IQKwje8 +RtLK0bY1KEeKXo1FCUVTg9IWDaNsi2tWPL8CLpP/Hq4Qx9SX32Io/3rJxy3h+sg+ +hN0x6m3QjnijzvokolnfxsAb +=RmVV +-----END PGP PUBLIC KEY BLOCK----- + +pub F067A2FD751AE3E4 +uid David Burstr?m + +sub 28CFDE1EB61BB6AA +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEYknmAxYJKwYBBAHaRw8BAQdA/xS5rgP6lF5fl8l+rJB1UiF+7KKDse0PmKYp +TNBH7si0KkRhdmlkIEJ1cnN0csO2bSA8ZGF2aWQuYnVyc3Ryb21AZ21haWwuY29t +Prg4BGJJ5gMSCisGAQQBl1UBBQEBB0D738vg/hiF3kQg4T8jjbxaqT6WMKxf2Kgp +fZ2RUnSSCwMBCAeIfgQYFgoAJhYhBMBhIEjzOTuAsiY5tPBnov11GuPkBQJiSeYD +AhsMBQkDwmcAAAoJEPBnov11GuPkQlsBAK/3N0pGcULvADnZT7Tpk0W7BDSBByrD +xvQy+f/l5tI8AP49K0LLVdCZ3ifAS9oHJLP18KhriSE+JZDYpoA5sWHqAA== +=Z4fd +-----END PGP PUBLIC KEY BLOCK----- + +pub F0D0AE433308B042 +sub 504F10B64E007C78 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=8mcA +-----END PGP PUBLIC KEY BLOCK----- + +pub F1EA1235E08E008F +uid Paul Howells (This is my test key) + +sub 3A6C1D1AB50A637F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFYBytoBCAC6gqk7VTn+98pMYTtEabjygjySPjp44jJxMWXPvuP1Zv8N384R +RELATpkHahA0yog7Cs3txP6zOfwsIXjNxx090U7bXBcMtXnVxN/hg8oVq09buVco +Rxqd2mp1qOs1TrSSltpZdJNbzsloHeaHSaA0XhQY7cLpbEtbeKvOMy/5B+oWSHSN +V2pee6aGvUB4F1LOW1OQInCGDextdRnFlZuMw+gpQTl2/FeX6QGtqOVI9m1Ep1C5 +mK18OtOjTCeYwi0jyYQYUepHudBWv2TxthYAA3mP9h5kqx/N3i9g4uOPtaygi3rK +KeTDpd7ppLVkBaaSjXg7+GD4UoIanHTSkcoRABEBAAG0QFBhdWwgSG93ZWxscyAo +VGhpcyBpcyBteSB0ZXN0IGtleSkgPHBob3dlbGxzQHZpdmlkc29sdXRpb25zLmNv +bT65AQ0EVgHK2gEIAMvZyBq9a3x3CMS2ROClxaM3J0geFz8uFr8GNEEE5J6mSx0p +V4y4oPvrWwKSED02tZR17r3vApslI2IR8BURKrIGcPRKF15Bsknqbj9jrVAWSL++ +Nw74NyjX7lUD+LQrbk/EQKiQAzmcc72Hyut2df+R4ZPB2on1p8geI4aAKKGmdpFt +/xjPnDN1EaOBgcPLw9j5iWlDHtduyADib0qAlSfYs/GLQUzXYoEne5twzzpg0Hr+ +afi+RKHl3IzyaED9UhUv+SWKIliYr8Z9AzhueK8Xz1FFs3pN7+9FHJi09cofebBc +14hNtluBz7j4SSSvH2GRgwZxJYpIZn2t5v2TyJUAEQEAAYkBHwQYAQgACQUCVgHK +2gIbDAAKCRDx6hI14I4Aj+vKB/9c9OaCIwB59GKgmUJub0nzQng3dL5YUSDmt6/W +PEZ3bRsHz8cL5+O9GWrWFkvqQhDmE2BcMjDzj+rIpaaDYIGQovplt4JOp7ARKA/4 +jDK0W1u29yXQntjVejZHlqkLvN7jB5ZWvJ+hEgYQn6ZxPaiB8RoFHWOM5xqAAwlM +R8E3Do9XwfRSPey70emDdU1u2oQpBoitfjOxSOHngMRGuf7XhxYgK3yj+vc231Wq ++ASxUF6H1Xwn1EeTLoz2WgDT27VJwvcgz2Hzt8jaS5KoEp/O1LP/fxH9EecvoV9U +Dr10EKjmCZkXJeFNuNZy+Cj/iS3wX70FpC88wyot5o2Mf5Hl +=Si2F +-----END PGP PUBLIC KEY BLOCK----- + +pub F3D1600878E85A3D +uid Netty Project Bot + +sub 1C9F436B883DCCF6 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGAhOxEBEADdB5Jy2sSOndOMCTyk8IFIJYPogjXtN7CnyIlqr4jEB5G87TJf +m7OxB95aIVS1vSA5ghCm88N1mKtW6jyYjgLFQbbyD9/X3ShVZjh8B2R4atL93SSK +ppfSrQE3+EohYzu/X5agtzMhg4VplfY67yBUFXEqTucXpYumKLctrYtOUgDCgs4s +4BixyAidsUxP9Uet2CsBiK7jlIe21EQz60QGvQ81pDaerwCxUsxtd4Fps+gSm6cY +7Q+CrJRmV+rGpOt2f9NAyGdqqy71tjd5e7VC6GHyDxiB4xnDKQDGpfiMtGnxHPfe +OaeYriCWQPpUIw7dg4eTVHKXlJ4FAc6W3Qdl0mlNKNIFizhcNxrie2FbLNxZYV+G +B3GkDZt5Oas1O/iWcQt2QcalwTJWBY35kSl+uZilDAeU94vzuu1SQCZqmTtH82oa +xp4eD4fqP5dB3qH/alao8IVlNRmbrEdbg2fZg4xVVmm+CF+gPnxswZRIptY2rsbb +oEM8dWxakT5zvjox+v5J+qmEkE5WLlL/DlokOnJlAjJ3fkq6qGengQNjlrMIZjcL +olHfr8gbYD2u4A7Dz9hls4fDz8OGqzHkSbNYm9hO9q5AWnqAWcSLPHkJ3mim91AW +enWzfqoxNNR6L02mDvippqpfEoFTgqmZvYun8r1qTU5UaQnz3Od7QAf72wARAQAB +tD5OZXR0eSBQcm9qZWN0IEJvdCA8bmV0dHktcHJvamVjdC1ib3RAdXNlcnMubm9y +ZXBseS5naXRodWIuY29tPrkCDQRgITsRARAAyUpCd1Ob68KQ2K+JNi9QmsKoff9N +pwLms7zW5dqHI1R2gw+dRyZZtg8cDYPKG3pQrlStSJJXPDCztAXESC7twCgiv+rF +c8u+a96Ex+so2bTgloj25sVx8VI5sjq2VviAmlMtvT512oN6MKVs9nRBNcZZQywI +LNM5fZVmRhb6veHqQVn649L3dCi+tbm8HywIRvinpD6VM9zMIk9ZLfenqQZZE7VB +rB/rv0bRng4W6/L3T/QF+yR6/1DGSxgmoysvu9MhWAiHxQZ7vL3k5XU1aNOf30zZ +OQAyTgWY3CDZEtDRp6SpoC+8ZkCPN+tiK4OpiooVi1G/9gwaRsmv0adI/EMPTrKb +CRjB1KO60x0puOtp6Fl746tpzZrvFW914+CVSbCFk2qA3Tgyf3kZ603Iv3jEupXK +GlV92KeaGrXRfP9l/WaEeT4NR2A0coom2bxq2UuVAKdKO2o6ore5dCZXPTHdJyJG +pQ9a3Ek6gFgNk1FsmAigW+HnOb//bsPK4Ou9lXG1VKFm/oBCMTfYSX4o8q1uAhjH +UaU6+vLRIr4JYmCw3Ery2GSkVJXSCiqTJ7TrVCslG0n8JSVCrseh8dmLIuNc0A9p +n4OvzMOiYU2uJb2xq1/Q+h8UtnLe6O+jubl1kg0eQE8JkLGLoNzoFSiBKNFLyh6I +EBQljZTMEev2cvMAEQEAAYkCNgQYAQgAIBYhBA010/YAeGVRJpCOivPRYAh46Fo9 +BQJgITsRAhsMAAoJEPPRYAh46Fo9dhkQAJ0OYPwfisgmhj6JGXBofip4nrlGku7c +XkuXwMzeNLrVCQPyccKBuwLHpW2evEECMxul4DbBWuXxKSXAt6PppI5VTL2jcT1k +ZSzu0zGfTdAJXcEgl3US6xG8fFMNaJl7wuXfNEH5Jw9bA4pD2YQBizyytD3zOA8V +Gy3nccHgywC1rdgtQaZzyhduo0DeGQa1AXC5V7ZDzqwHMZgl7MktcofojOYTSvrP +giffLTJQ8NVOqDF7lvJafQ5aCVy+8tdX7Qjza+73+Cfym2nwZCkz+gGaZe5SUNZP +9YWcRPlpNm0oEbqtN8G7NkrnQcNsSw8dIZeiU+tKQmnVrzf4YaVhsqjmGUQGOwca +x2BDC4cdH2MBx/Xf2moEx4qXmM/t0ugvvgt6RV6WtNIHoPMraYPygvHfuq0a9d5Q +n21edDu50O+KwFwcf+HvJelt6URuZl/tNGflgD2PXqbo7Rw0//eA5l2Wid+Vnqks +FKHFxVLydgeke7K8bbsjyLl5UifM/k9keWs9CZNk+wqpbVMf8EGnaLBat47L5fkb +ZwYpsGCIktVK3l+ZlWwWYhqU5WhTYcX3V1tV5pnxs/t9Qj8RCMxwOZGyc82gf6H1 +9yI0UkAfq6cXrZxglyp3JVFMq7kzGOVzBg/1Rgji8HjqLXcSReieclhK858ZRMT+ +WASB7yEJYwsl +=qHo0 +-----END PGP PUBLIC KEY BLOCK----- + +pub F42E87F9665015C9 +uid Jonathan Hedley + +sub 6064B04A9DC688E0 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEtsF2oRBACcai1CJgjBfgteTh61OuTg4dxFwvLSxXy8uM1ouJw5sMx+OKR9 +Uq6pAZ1+NAUckUrha9J6qhQ+WQtaO5PI1Cz2f9rY+FBRx3O+jeTaCgGxM8mGUM5e +9lFqWQOAuCIWB1XPzoy5iTRDquD2q9NrgldpcwLX3EVtloIPKF7QLq72cwCgrb5X +R25dB8PUdZKUt2TtJbjB+SMD/1UzAPirgX0/RpL9wUR1i14yIrTfpFP/yM9PE4ij +qcZ1yafVdw64E1k5W4k+Pyl4D8DvSJvbJHvYjg8/G9V66WzaKcv+987fetUuePvY +/rwxBPztqq8y6+hjBc8QVhZGWmAoGGEFO6MIGsSyN5ohqPMpNXkczIo+NMvDxGzz +ld5ZA/9awGTsigBdpBK2F6GOmbvBv+Xebu9rbaJvBvP+npNx01s/f5sHPCxmBTFk +m1vtaMdZ29RovrWPSZRj8WWes0bcisw80250r1CBlYzGzqEVZ7b0Hh2RfkfaxbYh +wikyfTfA2iX8TUGBgirsZbyegjUadElhwFNDASnvLTEuQKeVLLQlSm9uYXRoYW4g +SGVkbGV5IDxqb25hdGhhbkBoZWRsZXkubmV0PrkCDQRLbBdqEAgA0sZ0JZvWoKIG +b+o6MOwI6p3uMb+iWBwdYfoh2RPnUZdBwGhJjp32CiTt2Y3qYEcqC5NvF5FWdx1m +5KOQe1O+QFoqPKnC1bPj9uZOjLVql7x5tSwCePIaMNB+fMxEh5hYwLWtBz8nrdCP +gwm+nAwecoE8YfrpmrXZk/YLak54FOeEwLYaP8E4u2FHiEqN+WmKMjIRwLzVpYAr +WRCbTLhSSKyRBy7UxEovUH9mIa4YuU4Pb2R64LwopMHCBm5ow0U8kCw8vpW40GrB +c/2eaIeXCX2XJ77E9s9ZPgW6MoJ6Ic1xV6voLJKIEV8t44deKNSwDfVNZHxyemaK +a8/GgpjU5wADBQf/UzL5lXRmyTdJqRvHIfUV3g4A3X77d3vOroab8KKw4MFy2LiT +ioN7btKKxE97Jjp21YZFd7Kpmfu2i/kr9QVJo+DSxe2p2xcQozyS+layPK8h/61L +hyh8vjzV5AUWA5Zup+P7Jh/WRlh9Gxs0k0vimYMFKImw3mZr4EA8UCj2e85XIHNH +Bd0B1VIukq4OjU4QhRrutNebIy3GZ35ylcaXT5v18Rq/iRJAuJFoCzXUaE90/V9/ +2ob8A1CYEKGLocvOQgBsj7+2gP5WOP+WxI4TWPENRKMVchVBE8zV+7YZiahPCwOQ +r9TQWMaUIJxZ85yr7O8DhJOBX3B7EHIfpoADXYhgBBgRAgAJBQJLbBdqAhsMACEJ +EPQuh/lmUBXJFiEE8xhLzVX00BbjDUyb9C6H+WZQFcl+zwCcDKIILbGBUNHRGY57 +mmZ5xKMWbCsAnRbmM18GlK1TKRcOcqqEPWSusurHiGAEGBECAAkFAktsF2oCGwwA +IQkQ9C6H+WZQFckWIQTzGEvNVfTQFuMNTJv0Lof5ZlAVyX7PAJ9ztvyEP04cy6zP +9lHt0qXdrucDfgCgh1OIUk0pFzNYBt3PXvOeyD5FQbk= +=dtK1 +-----END PGP PUBLIC KEY BLOCK----- + +pub F5C81DE10A0B8ECC +uid Andrey Somov (SnakeYAML) + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGY/odABDADzlZ1BXT0zN3rL+z4HP8r/2xM6zN950fwRimBTOiT6uE8aQSxq +283R/gIgM+yQBGjStLP3k/TsFJ2FCz7sug+7s1RP70ymkshalTRg+9QHBr2MU1Cx +Xoh3fiH8BLOb3FIxH1wdAtOOoNxbz64Ftcptu3L0I1y2qEwOGNOyvqbntdCuwNbX +/zwZUyb3tOVVrrZ5bp+6jMoBKEEWS7effqhGqXLlO4yTMBXR4pwzhch2IGCe+4M3 +a5C2SIJbR70PMk6aJ2+no2LycYRYJx/t2umAbxuCtwT6t/xh8v5ekbXAu5G3h5y+ +T2bF8rjMhVe6DBgJ08uFge3Oom5a5uZx8sQASdLCng8nKjGO4Q8jWmsEj+OiHnnX +g0oKkirnWbAVrWysgNKAXfwGfDBG95K1F67kVhNjXTx0MDcxpsT9TPxz/nDuzRpQ +ey5M11+Bl/fEM5UuzRpPgPd//bU+L3FgEUguB4kzsiYlhsUQRyCq1x868AchLWey +vaVIq2DY101GIP0AEQEAAbQxQW5kcmV5IFNvbW92IChTbmFrZVlBTUwpIDxwdWJs +aWMuc29tb3ZAZ21haWwuY29tPg== +=9D46 +-----END PGP PUBLIC KEY BLOCK----- + +pub F6CE9695C9318406 +uid Sean Owen (ZXing) + +sub 811B3B85BC31841F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFHW7ksBCADGzo3LGVyWpBWqxRQlGhpQ9YNav7jiR5WSLnatr8chZx+ldy5j +KquO7GHO0qaHGXyc/CKeKH9Eey0mH4EcvOvEhBOE27Fnuw2NppxQjxXyhYTfvr6q +CHGN+lTORVC3zD6UkMm2R92zNI+ZGWqK2zND5/RZQMW4JNH/Y4ZA6t+fm+dHm/Q5 +Mn/2XEnMnDiuJGnIwb6+sgH4GHdXkzGl+/grayerAp52HWGmKo3TWWtxdZQcdZe7 +spaLlVJEfw5K7uwpR0JDwSHtak7gfs613n1VuQeT9ZA/CnBk0L1JkkSezPO0NFxL +ONjrQA6zBA6apJsdDQYgg55xtFaAWUfBAV37ABEBAAG0JFNlYW4gT3dlbiAoWlhp +bmcpIDxzcm93ZW5AZ21haWwuY29tPrkBDQRR1u5LAQgArA+334bZKR9IOvArfF6T +Yo1gx1wQjiFrbl3rcNrkADzu9/h5PbvkLma1zTSYUo5VZPIn0HbX+GctInY9AkjG +sc3OrBmPi2FI/KOUXnMCmd1ShyphdB5CJjG2VpR4ejG/I1YyMQ2ABWGes1IQJNPs +Hf1PXkJ2NA1gCxD+oAT9RgdXZBolln+TL3sYV4Z0EWhEL+yPjxInvFpabZErssim +tRRrfSuT/wczrLt46zTgmtEKJ7udp6kzC3Nmut6IozlBr5qcEOTdiH6+BxgvW4hH +uqANx4PzVWCCqwTxuiME/Q5kr45tgawSSoIsMAZaPGqeNluXap9qEsXPd3SsZUfg +IQARAQABiQEfBBgBCgAJBQJR1u5LAhsMAAoJEPbOlpXJMYQGP2AH/jkwFM70jQCz +uyMJNX4uHlmP37TNq8n2WxCNb5rQrXJ7UQ/3FSOiF86PRhOYAJHz0aEKWjQG+gr2 +aXc1HZr9g3AB5dLVxJ27SNgrV7Bvw1fI8NvYp+XyDodbQzyjavuslkf6BrQ9CSer +R3WahwNtscMXYCi08f9dB1hooKmjkqgHGE+WHvs5zxtVnmdQ9Oaeu7IYYkhSAFA1 +Pdb2T90L+0xno4kCXaN7Mlw2ffxV53eLDq3fCoO4wkmVdjHjNc6Cq2qa7ntPo1wS +BEhqoEjsNLqVXYq/cm7h+X5AwuLgkYCqA/TOjClEe8C/rVLVj9++Qw2lgbiUC6ry +lkXgEU5D+DY= +=6M1h +-----END PGP PUBLIC KEY BLOCK----- + +pub F6D4A1D411E9D1AE +uid Christopher Povirk + +sub B5CB27F94F97173B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE89LqsBCAC/C7QToaRF8eZgGOxcvp9aG+mFFCMjaRAb4Mh59OYdmUb6ZjfO +9388HPebGbPNR8SHYs0dBIuWY4ZJ7oUTYPswasL8vB0iPFdyHhvkCca+yk0b8ZBM +DmFlISm9HkYpoVjcFUp1oivyeJ5LRTJTd5JGEd/SWFRbB4TimdKXBzej9fIm2zVl +KInEMMd8HnSYE6nm3aNkbyiqhx81bFvl8x6X3ZMWcKs+TAVXdP9uLVvWowUwcApk +xpee442Ld1QfzMqdDnA6bGrp8LN8PZF9AXQ9Z6LTQL3p9PIq/6LPueQjpJWM+2j8 +BfhbW/F2kyHRwVNkjaa68A544shgxJcrxWzJABEBAAG0J0NocmlzdG9waGVyIFBv +dmlyayA8Y3Bvdmlya0Bnb29nbGUuY29tPrkBDQRPPS6rAQgAuYRnTE225fVwuw1T +POrQdXPAOLDkiq49bLfcxwRJe+RozKrJC1iKxb751jTozEEJLe5Xj7WcojqgDsuT +jzaLHDNvDCzRFvwfkJ4scMTAZd+2GYsC8N3Gg0JRgC2lU4wZxsanLnVMbdX2L0lZ +7WnH6S+GJ5f0Et8PM/g+V2Gj2UraBhGGak8OBQ6NhmCJBcyYg8Bh90cgD9V1hMRM +LSW7gB1vnpLM7C8Yymd3etdZSIltmDuVb3uG9s4Uwq51s2MEKsXsuFYCHTz0xT2u ++6e7Puaq5V0218QGR1Wupkl29iIUF57hFR7f6oYKkecvPKc4Yev6Ii0Mbvc1H19k +LOXUrwARAQABiQEfBBgBAgAJBQJPPS6rAhsMAAoJEPbUodQR6dGunSQH/A+4/Zbr +2jB46q1JEN/UV4U3MBQiNvCOSD9tOPMnBvVzJ53HutvGGkmafbtbwDZaN+YMs6fi +itBMqjF/eQ/pJ54aFguTPGMFrlFyjz2n/pffkHLpVHgs8V5M4ALITttwCOo8Vv7u +3VjO+ea5kiCm9MqJySrUP2Dv4lPVB32eoEUqWDxoyeACihW+Utdo8TBDVd+R8w36 +W3CUSvujW2z9jMNTF+VoVWDQWc3up7Nqb+ztW9wrjqs73nJCv9bLPahUPNzfh742 +v9vak3TkwMcDR1eZv+KvA8GXSZM6ACALzTmqRHXjGF3UZ4vowQDfiTzZKr87eBaE +FoHco7Lnn+W+8qk= +=9+x+ +-----END PGP PUBLIC KEY BLOCK----- + +pub FD5DEA07FCB690A8 +uid Baptiste Mathus + +sub 5F68B9B2F1725F16 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFFCLwoBCADxtcGi0nfolr1kGWe3jQ7n18roJFwBs4Q52nx0h4+a8ZGr7/1E +1brakrz3t/cTSZIrhfru8kirP8cJtXBxpd/nCeRrB/4ZtXPUJiGwKx6sVGr0ix6U +eZKQb3anH3jdGTzZ2csqt6Ro85SvIHfqAREcPOoQk7Kz3DFOGbSfECN2Wf1pTnRT +jdF1Z5DkoTd8sGBmGOdhNMzgqMTHz6np6M2B/JVP5DpcKPbbMhQ75RPcxNEb4QSa +HtCL+gMZiF7fndWx2Tjbpanbb5+TomfWOMizpkyziyYeUmGoyggrnXQ4BMlzsIuK +ATz6wA5j4qfRLfoDDgNv5UacVAtWL+rlTP8NABEBAAG0J0JhcHRpc3RlIE1hdGh1 +cyA8YmFwdGlzdGVAY29kZWhhdXMub3JnPrkBDQRRQi8KAQgAmiucGnaHWRi3QQAH +EYPa8Acz8vhe2V3cEANZB6NGesYUCT9v19wv6xIClufUIQS8hDATxC0bTub0wuVG +C7tKRytF7DhGnBVU1hkPoCxqdlJEnACRIAED2DTVqwM9DKAD0P/0VtgcqYflgVbY +Jz9PZ5qv1nF479Lc+xa0hVvwNLgA5HBHVhseiLTYh0QGQYDuLs0mPBlMNmEhAYwI +pQ8FbPyX7Y+XZSZcCCcwNFczSBbpysAU4nDB0kh0TYnPs5cTDrNNmt1ytzQT7/Wu +QyH32blPeue6x5mTPc2ktT8h7DG6WbMvoRbcTBX1blYqI4Ot2QuXofQv4ISgiGz9 +Bfi/bQARAQABiQEfBBgBAgAJBQJRQi8KAhsMAAoJEP1d6gf8tpCoHKoH/RBqM5x4 +DsH6IXFrIuriLi4LE8qXdn/szwGKYMkj/CytvlGQZ086Os8bVgdGWQI/CAnXIlTn +XEmIpv19vaUc8oCmAyaR4e2b78XgfW8YOxn3fDJeWHHX9cfC/z7IvJGo25DHM64Q +pcYQEe+mao6ZuoL8onvttX+qgJ8fQfieY0KTeqebi4NQHN3VPpGPTCSy/Ksud8l8 +SR33BfoTKAFWT2o1Nn88koi7bRDZ8VxyKiYmI/zed0SZnEHWCzY0DcH6atNQyctJ +OZ71wxgmZoFAdk+8NbFSCBdcNLDMWhMvG5cQfo9uW9yoWGHvTYkLyvv4LYQ4FsoZ +B7NlACz0MCH3fYo= +=PlCZ +-----END PGP PUBLIC KEY BLOCK----- + +pub FEFE78456EDDC34A +uid Mattia Tommasone + +sub C3720DDC2E713B7C +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBF+y5lsBDAC5h0qk+OBAscHc/ac3A9C8ZPohXcTVpsOjds73soUAH+QCKO0y +gAUuG/hUUU9xkm9PgTwWOEl2qDDcOFXY+9ykeYNUUcCWfs+JmVRfRod4W5pntaT4 +g9Z+T6LbXKNAfZgPvTv4rr7UjD05N4XS4vckrS4taYLtBRJAmqT3pt43KxlyoTbh +f4xcO2rWeXsPqgzTYIHH7M5mYPeqA2gc9NBAhkHjesFuYfWXqUfOVcOLvzULxrra +pZDOyrINr83WikC8DkuDrAav4mIWjIhYmfBWzuNeYJsusVnFENeOxpEHV8RT+8uE +v1gPjbjAKUPfZoa7egvz3EmDkshpNIIym0XxNGTj5ntJWR2SLT7mDrSYPeHrZKW6 +/aKuAcOxpGLpdVOMM+y4N5mTQfdlL81G9kbGQarMmwGaJb2a82PaF20wRwVgiVfO +p/GWgwXr0XdJNLqx13LdM8BMM5vmLomOQOjnpQBOlJWRgrYUJQOReKAEAQqNMsxS +IW9laXkrewJtblEAEQEAAbQtTWF0dGlhIFRvbW1hc29uZSA8bWF0dGlhLnRvbW1h +c29uZUBnbWFpbC5jb20+uQGNBF+y5lsBDACv+jA4LF5tkxOOn1yhSwOMVpsmjBkf +QX76+6HvdRj+/bP6+rC6Dz0AGOs9QhxwT3+3l1HISMG3QQPYoUzeaLr3ZCHJgTy5 +FpQpbPSRhow+7DRbtFNuWGFcSsGuivWbTSJs0MZJ/d8iv0Vnu7l6n9FMUMINpmCq +1ZAZUP64ueoDkQd3BTKJ5YNKB5OFF10zeEpcHV6V3gkok/NDRfBcuC/wyZs5z1bm +nFvVQsPjizXtIoOOUG1G+tJF2ATGB+kpTrccfQPqaf6Qk/TrqSdh126c3DsewqC/ +aY+51NUhBdgZvzEMuE/pRgmjB28kRszy+nW3938KAIxfJVUk6VNnB0loQITSfiAw +naKENnypsOXzU5CFOJa+Cgo4UctPBmCbKEqm3fpG+ReSjLCqUo0ZplU2L0K3VfF+ +muPiyjMDkxx/wEBrFdX2xB9ksofs2EasXNz+vW/ZHftFhZ+Zgvv10mPXGytwQ3zb +pxKjIuLr35c7G9t2jgp/KilHEQJN13o6qT8AEQEAAYkBvAQYAQgAJhYhBFmwYiT9 +iRLjZgO+ef7+eEVu3cNKBQJfsuZbAhsMBQkDwmcAAAoJEP7+eEVu3cNKJfwMAJOe +VNi01GBqtS3gz85GUGBngKt04cvVL1JgG3ov6YCJ9fw4a4WSQIhQU6Q8w4EJ+Nr1 +64IIpv7N5G2mhLJSJVcUzlA8G8ZFXPiW6opYwQFh9HhrqMA92yrwdXYzxGKyhaXZ +TMXZfGryjuHLs8YMQfSXX8giGZLtvJSMkfJsM5TWi4bjlMqpoJ3P8qgmTm9dni74 +tdAWWP0VpFiDl6nD7mz7IP4S1ntcsl8IGCD8bKmnYoL9IGIhXlBatKqPra+hyNrI +iGZQciVA8vxmVFBGnuuR5oCCYDiwuH9/BMWjyj1sGlN+dk7HI7phL+kwwxVpyTYW +Oyt20j/MShit+/cGsDEXkgHgnKi7sxKIgenBz/ECSGEjYiLioZcxnIdBYSfdfDG/ +LvFJeNoNu0g3HZeNNcvKRB2/mlJ/HeJssV41ctc0sL3F3ShOK9NlzY4nNn8yKtv7 +Shma5nGIB6R1N366OBlUvjTs0ggfYypbVA+6WqpzParu/r7S8VozcUwcNZt4Cw== +=Ru9R +-----END PGP PUBLIC KEY BLOCK----- + +pub 012579464D01C06A +uid Herve Boutemy + +sub CB6D56B72FDDF8AA +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFgnlA8BCACVtx3oLXcanfvwtMRwal6pLQ8IVMG9+fr4xGdbSHXCRNbosDa5 +agU7WeQMPhusSxJGaA3w7NOdjAwD/LeHADhDPeI6llJg1Fb3EyqH0NZaODKU/Or/ +dID/i1onAX1dE914J4lf3XvIAxGiAjmr3UvWO9RiFxRUkecMAMlCBp2FuHuvxkcn +Mk8q9dP9Ef360wu8X5rj0kgP6vPhgl9/RhuPsUxlazb2Kn9Zxi/RmDKDiH/vDuwy +WdRGFOR1OPV7l3Ws01nrs4vKd2v5rsUmsjvQ8ldxdrA1xzX4IszHRDgSC9PI8ItZ +1VlbaKjE0L03acPfFTg/wRFSF5zsrGNbTmq1ABEBAAG0I0hlcnZlIEJvdXRlbXkg +PGhib3V0ZW15QGFwYWNoZS5vcmc+uQENBFgnlA8BCADBlqkE+XHE/0NTsqaDkBhG +Z/qEZIBHZ87fJo6O2jl3eqZDU5Ld/iwpJm/D6d/2t4mBH70rwWW29iW2PcZ5jPIw +bnFp6MNYsBD8RoVLrt45SKUbAsC5PrrS5DAC0ZKKF6mpw6FUVsblXhWDdGrPJ53Y +FoiU99Ip5zeL32kd8dxOv50ue86eSIIMWWLF00R29uZGqH/ZYJR8l3sqVu6rijj+ +EnhEu9D1VVJ2GQZS6Z1/GtprEbSYqLlsQr/5B52LI1utr3O56r1gLErHwU+tU1ce +8iPQnJFRQZ5KRtSPAvVqX3Efo/cmrqbYFDH63w0xYNwIqe3MzEqC+Cabu4wXF8vV +ABEBAAGJAR8EGAEKAAkFAlgnlA8CGwwACgkQASV5Rk0BwGqRkwf6AqArLie+te4K +XLhAF8VkwX3FyqOM/DmwXpNkVIdAGPWl40WEs8MG7VRUQtNRLK+0fW+UnGO1tUw0 +ASi9DkkWd48Mu/4QO5PD+8QKd5guPhXR2hzB/Jxs9iG1Ixlpd2KdNTUZ+I3PIHJx +56nFH3+z17ETFLSd3Z27CGDqwQG1ipXdO0VREFMmn4FH3RfXMXuj/7JUmcQEya5S +D73geW4HFQzDMNVEDaiS/S1j9iDO8XJYzgR2O46sKp0OKUREfdlc4S3bbHcBdB9U +rhEaQ8QQLom3ITKAn0NxxEfitpk1KCdOMaIOzELNmmjMEm3ptSzLzQWf5nxe0DGH +zFdp+62yJA== +=hl8O +-----END PGP PUBLIC KEY BLOCK----- + +pub 02216ED811210DAA +sub 8C40458A5F28CF7B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGADx6IBDADoHin1LGQ8dhnlhfNCBZ3IyXS2NpR1VjmYtHSlh1hGsPcmHuwo +1mLA6JzXF7NuK3Y52pbTr6vz9bAap8Ysjq/3UJeiDbf7FvmO5xAEVUhrpc7AEY7G +Wygi+HqK5OaNhxUr7OmHY4N2/NxXiYGD2PNU3mXkOszpQJk3yVKgjmGnv0zbTpn2 +wwsXygc87nG/h2R4YQ80m9UknkPR63vRwPnsTwovG9CAb8RyHq+6P81vKE/U5GUJ +TzV1BDY95niypsCYja2QR4Gi5TKlpsUjT4sT32l6/CqOhcpwO05pTv0fvoHDbDx6 +/gHivgyVUyPbQzUwYfMYoINePOaX37okHQE8n5QPPx6HmXfIhumKbXi6ppVPjPG6 +cB2Lq/F6UKHlttiwWgSIiLDC+UbFCVvc41Lrydbt/2eXoBGxWbU6DUSGnefKymP3 +c3IsgdzeP11tlfaxLVz60lomXMeyyXD41QzeuyzUNvoSgiqSE6BO9EqeCyk1/n+O +Are5EFnyLBjChgkAEQEAAbkBjQRgA8eiAQwAuC4Z9laL4sRX8FTseTzd5/8AqBKk +gtrZjW5onrse1hWpkjeB42qfhVrfUorkpGY9N0xo7jZT7PhXuOEB1WRcJPHA11Q4 +166WkHRDv7IwPGAQr6LsJAAlZYkV2d3BXoWoS4ATCH1jyXaxKT/jNGBazs+Nqprh +ypL6X2xOIqKozehjTMfD1cFzFzoaZvD+G9qdk0w7qikUIla0Y3ADswtMLH32mszw +9g0ddFSimmWQ8scVcaalt9k9ATX7zMJKmYaYi6fWsH/Le13DhJgQMjjh1BeUguIP +r6pRoBZ/5xJxJ7OKIRk4pk6h7BImGMKTCONICf41i4kGsZMoRb2XvLDgSNs9gYKp +N9+J7TYTeqofBxxQLH6cVplBPoNCkJun6scYJLWAepr4u0K5RTnU7y9iigiTTFeV +xbSjuxIEzLk9gVKD1hsbtkLVmkxMljqJG5El3I7qu7eM2c1ufo22BFjHom1CmtWd +oai56nxG5zv1WDsMRJukaXbDwbpSkb45rj09ABEBAAGJAbwEGAEIACYWIQSFaclc +rcUIsJ/pDzACIW7YESENqgUCYAPHogIbDAUJA8JnAAAKCRACIW7YESENqpGYC/0Q +NoVAXMkCa0Iei/kGdzZNLKpiG0nZIJGuml9T7eMyp0QQXzenOahCGhna4QQvSBER +UZb9HzP/0xY93C8FEXv7Ns972XdeOvYjpOLG6euRwWLD//c5Ah7siSgUJ7CFPBHj +r9mnZXzYjhvXT0eJlb96j0rBuSblG/NXu1oEJPySqP7vkK2ZZsHNoGfSoGlGtush +YtUP568KMzz4LsnOfSLnkOc9Hh0qydipY+ocfQQhh7tLUzFsMbG80yWw4/2JVicT +nTosdl4J9WyI3Xuqa423XEAC25dS0aQNeDa4lpfmOOyj5ViJISdutlVC3zmtkpXE +xUXqb+AcsNDOuulUhVjw7KpKX7xUXJM+LSg57lfyGHiLejDHvPAXBSfzFxT9ZDxO +92MhvR7JqP1Z0SvZ/yZ1RAidKaNJs3o1Dk/WbuxnRYjyf4URhfUVeH8tykNDIMJr +gY4uKjJu0S9RuzG1PVw85w5f6UDZlJ01gGvtT81JFrizhvS9t0HoPbDcDhG5iVE= +=Lmqo +-----END PGP PUBLIC KEY BLOCK----- + +pub 0374CF2E8DD1BDFD +uid Sonatype, Inc. (Sonatype release key) + +sub F2E4DE8FA750E060 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEmoKU8RBADEN0Q6AuEWEeddjARAzNXcjEx1WfTbLxW5abiiy7zLEht63mhF +kBlbyxEIRnHCSrPLUqY5ROWdyey8MJw+bsQn005RZmSvq2rniXz3MpcyAcYPVPWx +zgoqKUiu+pn3R7eldoDpMcQRsdNbK4TOFWNUomII70Lkj4u/DP9eko6xowCgvK/R +oRhshwRoxJl1LauUFuTeVHUD/i5DryK5j/P9tv9BWSb/2Jji6gbg6Q3MThZ+jCTi +leOHR6PSqajYphOFaA8xVWQAkvbqfSps9HnmdFJ37zxOn2ps9d1L8NLoX1GMu7dv +UZkCY5hR4gwaAk5YpyKa93NpaS5nX6beKiCes7lDy7DezjQLZVbKI3Vsd5t70eTW +tD7JA/4lGUSkole28jxo4ZKKkGMFnAXkV5mWeOTz14BibW7JqhkiIpckDfyq4NjK +ts1EzMmnXmPkB/u5QHHe6fJP+Laoe//gP3Y5+xlnAsyI4iEfEjydJWiSNx48c/2l +qCQ/wdOb28xoFavdCCBavqSKXKJREHXul1UGMICpu3rq9EOk47Q4U29uYXR5cGUs +IEluYy4gKFNvbmF0eXBlIHJlbGVhc2Uga2V5KSA8ZGV2QHNvbmF0eXBlLmNvbT65 +Ag0ESagpTxAIANEHh8lujiAJyotn/aMY57BpYtWH3ia5xAA9CsYcMwHzvXgeK5OY +Ao9gydiENr0IZxa43AnZ4WoJ96AHLX3OtXJhr8jdvfEHrrHpH7sHgEgrlv3VYw2H +ZSU9cmfv+yviC7BSrQqa6LfOwSaWizg5ZOrCG7J2FAKhCnRdwa8ZOs2P0/Pu6asB +4G03mnVaR62ZShntFx4iSWlO78caKUQbB5OgK8oYA1k9YA4EReSwZMlKWpfGOqgw +HGw+xuRkXUObzlhaTWvfXgPr5RsxZzIviKH4EXSfdiIp2QzqZjM/drmfIx9r6Ai2 ++c5WKOIyt0WaX0HWOGsh1t9bBPs2FrrO1H8AAwUH/RxVVWYuO20H8oR1bp5zkjof +wgWk7t6NSXZu7mZMZwqaRgx7gXB89Dq4jALw4xXsAWebgYadReWef1ZSUlx8Lz1M +KOikCAld18lvqf/lfvrHi+ZHInNzqYcAdamT2BKoXHhIIR5jf6/8Flf/FsZGe0sN +QJw8Kl0yYiGYWhI0VV1CIzeVqSDSLLM/g4xZ07u+kt7/VUjo05lFO6jyCNxIO3SR +IBNML9xNXfZSDz21GtKW2ve9+bjC6qEDa0/O5FykoyoUFA9LuW7t05i3T26B81AV +mB/9NMxKHcJI1itU8We7L+IAvKC106chw4SxhXYxa/G6fxTNlkPSKdcPGsdFALaI +SQQYEQIACQUCSagpTwIbDAAKCRADdM8ujdG9/auQAKCCtFUmslioIPSjZkWVLJpo +77IquQCfcgYbAHsCuMFkHkZ1oZQA/f/ku6Q= +=YmFm +-----END PGP PUBLIC KEY BLOCK----- + +pub 049FE94F2D5DAD9D +uid Eclipse Jakarta EE Platform + +sub 953E02E4F573B46F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +pKNG3IY= +=QfmY +-----END PGP PUBLIC KEY BLOCK----- + +pub 04C39AD208277F6A +uid ElyeProj + +sub 694C56B594A809CB +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +I5I9W6O5fouFOad310U9JbvahUgKflaZ8LYqPA== +=aZlX +-----END PGP PUBLIC KEY BLOCK----- + +pub 056ACA74D46000BF +uid Norman Maurer + +sub DECB4AA7ECD68C0E +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEoo3BYRBACXE2oGRA58Ml6s+kvfk6n/AJ+5OFeRT/Xelco/cpdxOVF5LkRk +yd+vR2+F9ldBlH7CSTCmrdZIN3M3zrcWndrk/OQkCxNWVnE/a1li7L3G9nYr011k +MwMM8MLkdf1Wr+FBunf1qpxPYuydfjWGFL749hYr4uQ8RbFDRQcmWLYCRwCgl+ur +E28AmiICPcje59DNKHZZxd8D/Rk1LcZojARyMPjEsPOVSOh6kOaJQ/FOKN0j97k7 +ZqA+4C+OnIONSy22uMia9xO5g8oMLyHaRiA4S7JSIypYfX7JMCmwQCSLM/oQ5zct +tsY7tGzCRBA7UVmW8uCDDZGmmzYIGQ7h1vcabgOFQ8wsteMHW3F0tU1K6oQut71x +5KowA/9LeDjhl3tKizJn5hKf+NR8kTMcFFVMk8tf9/ZdqCG2gVTuB0EFimH47j1+ +YFWftvKg2IwF0qRnYuhpXn3kAtkzSwDr2T4r5CpDjttq+oBwhJ+N6lcPRoU26ijr +nQ61Ek0jFFE5vfU7UODSLYXYbjf8McM6BtksY1SWfFBU5cVzgrQhTm9ybWFuIE1h +dXJlciA8bm9ybWFuQGFwYWNoZS5vcmc+uQENBEoo3BYQBACSBgW1YDdTu911T6Bp +WXaj3fZ+UDoyMvXBeoeRgVxFA8t0/olYdWpOjqp9YN1ndL9l4EONIClnv/DtzBYV +pCWyxDSoRsnPXSyKVw7fViCn1dzzW25SPxZhGBaniQzWVH2n/xdy9y8NGdmBNX9C +q9yeuMbsSQAlIwM15LLBKlVEOwADBgP/U32vKyQ9OmqRd7ttqnewzWqOlJVHqESH +ZbgqR32UNdJxA05xc+cDcxwZWQw6GpHrVGeSGO9oUSjHCzFkSOTGpLUG/V7xac4Y +hqZbBRp8wgSPfQKk7Mct5uOQHVcW3Fcti33ofYM0vmZy+puPV3+5kLqkCWrSF5ls +qZX7clnN1w2ISQQYEQIACQUCSijcFgIbDAAKCRAFasp01GAAv6p0AKCP/EDLrjxq +74ryg0wpNrQOtMOdYACfW68zcmywrNR2KD7Y2Pe5zhMtLZs= +=fr01 +-----END PGP PUBLIC KEY BLOCK----- + +pub 066235F925255750 +uid Karol Wr?tniak + +sub 88518C11ADAEFC68 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=W0QZ +-----END PGP PUBLIC KEY BLOCK----- + +pub 0729A0AFF8999A87 +sub 6005789E24E5AD1E +sub 6A0975F8B1127B83 +sub 3FF44D37464BBB7E +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +fVJykhD1P8/4BedOSN7DqsnvIUfFaQ== +=iddT +-----END PGP PUBLIC KEY BLOCK----- + +pub 083891AD4774845A +uid Eclipse Project for JAXB + +sub 8118B3BCDB1A5000 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +mfMjMgP4QIPQl4W7IExkjfLxmiDnDUJ7XCfM +=4S73 +-----END PGP PUBLIC KEY BLOCK----- + +pub 0C907617691418AE +uid Markus Junginger (artifact signing) + +sub 0BD96E9C45AFAF0E +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBFEhRX0RBAC9Fi/D2LPh2K3BAqLQLf2T9uQDimZFWSj0SzRN36ha6yRjCKO/ +yRQ5RgtY0E+ZMHl56sfvSSO9Kvkd+IWYAn4MJeMA2aAoXGwJ44JQNgvAizY9eZO1 +BKcCW2lvSFhzljp9ltzCx+pevbzZLJb/AlEV5kddwZHO4LyDIzVSYrK84wCgp3df +UycGM2uwq3VzeNMOd7IKS1kD/1IxPw+wTmcNLDEv5xXbWO3aIAek7UP9cMJzWSHH +j7PuVs8Vxh2vLSzOJp7yM5e0kcjlTK3jtCrdazbQCIN7M5AWN6stWN3zUy+/QypY +Bpn3aatNN+N68V2g/JAa3V0lbW7R+KuvYRW0Kjf1WrmARI6PDgHH8WIrcjr8TNzN +7xl4BACpRNrAFEp/sBeJ/Ofz7H4MDgy9Q7a8pubWy+G2tJHw+lCyybhi9U43RQxT +gNwnwHmq2SNr4ZQF6ZWcijf8GNqvchIpehv1/dlCnOysDKdFPtKNLsBygCrSNvs+ +mh/nacEz0VivrGExIAJpA+FQxYEUfsuEuz/QjM+kDoOdGEtlYrQ6TWFya3VzIEp1 +bmdpbmdlciAoYXJ0aWZhY3Qgc2lnbmluZykgPG1hcmt1c0BncmVlbnJvYm90LmRl +PrkCDQRRIUV9EAgAkPHfvOSxVHeBUrm6rn4hJ+r5MgYJRASH/tFZqjENR3b+470R +62s+y1NkiCrL5JJSbdnX43gt9s6gtGdX355xRyKfkSESWDc5rfTRRb18xmMiWV6D +MzNMXIyhTpZwDyuh9meN/MhXfKgJLTQqwgGESBhwrqfBrGffTnCyP3bpCFgJZs51 +CLYxDVAHtnNYUtsWBqbgxJ1Ay3SOi9zzI6BwvLzQ98Ikh8VSv6lNrqVVL/0Usv7Q +CMeahhdpHu0ZRAO/ZM1UAkjBpXtxkcwAK1zmNyovzQL/M+MPvUCQdBHYZbkTSuH9 +cxrLk9qFobMHHQSYpfXZ5PGs42plMkAsFP23BwADBQf/SdSJiEhQsZXle1dBvap4 +IvezcMfeqb+gh/K7UZmSFZu4dBAUi0apDpr/Y1Z2Meo2lgQo/oFQ12eedRgwpJZh +j3EY6a50XcvWkT3zFJLq92eEEQrtBHG/0BSAq3jNGPgj8sM/+cbw2eCqH4FQ+q3o +WBBDELzFKRTWs3Wl6sRw1xTIt7OBAHSDIApIVzlAkfL9aPrSlvB7LLGulqAFXsT+ +auoaeEMAmKNkg4AcJRv13hBBwLGTMoQ/R/sPFxyEh/mQCy3baZcbL30WeRp+1lA0 +F5gBPq1J3vk5/KlwXpMuZQBbao2SGTSQLeWhgzBcCpwYeQGMyJIt/N7tKO5Npqc4 +54hJBBgRAgAJBQJRIUV9AhsMAAoJEAyQdhdpFBiuk7wAnRGeGULQ52vSlNDhwZHh +9dVbfql5AJ0bDDsu84BtR8KT0LLx2TA4UpHuSg== +=dGO/ +-----END PGP PUBLIC KEY BLOCK----- + +pub 0DA8A5EC02D11EAD +uid Paul Holser + +sub 71499A87DC1FF84B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBE3LMfMRBAD90h69D8yyPWaSoAyh2mOOOZ/XH0isuBpDZCWptemlMHgImqdQ +2sXLXYT1bJKmSaMw+yKjp8J/NYk69EbmSK1C2nypLQtWhUmXXd3XVYw6hrG/dGvi +gjkS5eq8L04f5CSuXO7r1eLTOch9iDl/ESaI5Nhq9A3mRQIhQalum+FjzwCgx0PK +hoC4MVPVGXzOQPc5sG4jzAUD/0OX9c/yKQqjHUs38HTCgrjseM40iPhp8NfbWenP +IwUMSWXE3lz0MMeKrGcEOcZOGWsjyepkLHXwj8DDOeGxhfh9bhFRJssdfzOCKBpf +6S70A3fanzqOAvddxCqF/zOwDaieDmWvVntVpmZO9d+pgR/sZN0JgSIm7qGDdNqG +Aq6eBACyywfwuVoY6lfNz70ZZqYjYuUkbKnKqpG0XmI+m5LYrUW7QuKJqaNdg+ZL +vVgX/TdkWVSIoSRS5+eYm3fRD++cg3ZgoR6ZY4WEa7SRSp2zoidtQijWOzp8WeHs +30rIaXBrb5wHR4GQ4FYsBGRuWkmIaferusllUmHCHFJI4ldvgbQlUGF1bCBIb2xz +ZXIgPHBob2xzZXJAYWx1bW5pLnJpY2UuZWR1PrkCDQRNyzHzEAgA1rD70DvCcy91 +ShQKP2snZ4cLJnFwKArulDUcxoBZ0AG0qMbaE8jiiJTHIwgVrqsKaS1JENv6tVdU +S8xHswu30zvd0obaj+4IGXlMVdc6052Y4SLAGNbGVw99Ah1OkQ7ov92gmYAYfqpp +OtRt1tylz7Jf+F6er0umdVBZm8fJ+QjzTw36AwERj2bjVbc6ogM7OsTyru5oZGOJ ++oJU+M9/mh/Gq3AyrcfU8c6bL2pacXwylME4lxy4fBB7BsMogPXXiplZ2XRH7Rml +ZiAfLHv3StxVB1RrLXVcjdnqIO2guVYrK6mxH9WMG4LnOGvcnYTfGtWexEmNA5l8 +IOSgsK+InwADBQgA037clDFi3XWaZBVXst7IfkU6bKq9vhmphS4fuBx4wp7MBA62 +k0kGDmZWcbinZWnybypili9ihYSHlp3EYzCNTbUMWlbhc/ffYHuvrZsIT2DxMPb2 +iCnjPu5HMGegTM8iTTotW4xYmJUsEDIvgQUz0/UNsPHTX5XU09SocL3YOP5MxcEb +gO0Fpjny3X76rc+ETAd9TmDJi7HOm24grKdOQXHQJr65j7nTc9M3zWnTxOP3fL9j +cVnGTnLGRVoR7kedDpa5FsoFqtY8YMaFvNPVvI4+m+jozjNwTg2dGG6nU2dEC0qg +DEeKMSJwF1wgO3Fe6mXHvxratgNrqfdY/rtEGYhJBBgRAgAJBQJNyzHzAhsMAAoJ +EA2opewC0R6tNKgAnigkHDCNu7Owm8x01E9+aL73JmDXAKCj7ROh7Wu1iZQbjeJf +ypM6CQ+fdw== +=5ywb +-----END PGP PUBLIC KEY BLOCK----- + +pub 0E91C2DE43B72BB1 +uid Peter Palaga + +sub 83552A552A0D431C +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFBIm/wBCACgqvegptBhfKbyBXZiW+7XchIJCOpwq0/9QgSehKMwELbUKqNM +sIVrywANqYn32S9hNRvBiKGm/KY7VwN9p1Cr6Ey3XuGSbRo/xN6tqfV/rV5YClL5 +6sMc67BlnEaCZRNuB9ATeUE/4wCO7fWg79jJuNl8tKQ8EYIrVGizzjmZHt76OwAi +hQtD6A19+qjQ02SyPUJS6a2lKx+gwaHNxv4L2FqImCFGOOEToyRb12GD18Mgbf5o +OtQVVtr3qbT07odFQt8Iyy1DiNUJbOfC+YO2wO7eMTr5xaFr1HejsTvKZiTDC0Nr +EjtctqGxrjxPmoUPNwtxwEDTEh1lyKMhnqgJABEBAAG0H1BldGVyIFBhbGFnYSA8 +cGV0ZXJAcGFsYWdhLm9yZz65AQ0EUEib/AEIAMDUgjnPKBeHIN0KNmXTS/uXXC4L +TGltnQJ57OG2kmPz/JjAjYLoLvINY+xtghehMhRY3DmQDy/ufZsgO9oH8PztcC8Q +L5/dV6VTYf4U3FndbiSKgikaBX7yu5Qcrtkv8XgkJ+awIEUgTGDXn2VT1hH6yEG1 +tA97iT/d7ZUxLEBsVgbxz9VtPellTNK5x/8NGY4NW+fM6+yGFpjr5juZVYRLa8u5 +65vGBQO5FU7bg/69DftmL7vO4KRLs154VpsfAsTeo1rmU/8kIjgCVeKFClJG+Sg+ +m9rsJNYgiKy9dGfD/qDmVlEeWBuhtlAfqM7pHTv1Mu8mv5/DheBwvlwheg8AEQEA +AYkBHwQYAQIACQUCUEib/AIbDAAKCRAOkcLeQ7crsaE0B/4/+ZcjdUfLPlKk/8BH +0tMafEWOGvqY8bG4YpxGoJZHT/Lb/cnWDLvZzs98FVaQ3DKHZwQhhtnQIhnupvxS +HX5wLeBZMtAANGQLauGp+A3S1WBVRHs0mzOdlVDbzJu7RW72mnkRMSoVd018fh4e +Q0+VpZh0Pf9KfKJDwpEuESP1+6JcLLBvQXlEJYHOk7Up5eRkhljdIwz3TlSuJ9sC +scTgM0PI7/L1eFP/iCgZIBHhpllVV6v5IGXx3P5Q7YQUy32zCrht4t9fdtdLct1j +6eNaAQdPAU91auSbYhuVCpjgKNpwOv1ULoSWLUUPMNW5Qc4ZDKq+ywOElvONMnX4 +oaQ1 +=bkWq +-----END PGP PUBLIC KEY BLOCK----- + +pub 0F9FE62F88E938D8 +uid Brad Corso + +sub BF6D15D3F1BF7BCF +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=Cl60 +-----END PGP PUBLIC KEY BLOCK----- + +pub 10066A9707090CF9 +sub 2B9F5DBAEAB53FE8 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFGKp5ABCADTyMhDq+7Kcv2wXOpOmZgp++JNO1erNUjVqFX7n9bT77DciEML +LNxWVF1tkNqgkn0ughZTXK5EGdjUfZaJaDDfG4BIsox/ng4nDvIp4CtXqHbWqrlc +SPsMl82uABh7ZJgsZM4Z7c8KirxYL42MiWu6hhRqfJZigWUd+ceKEDuFcNHcEA6h +98LWFCZ7KHjKhZpUbMBurd5f+N2mn06YnVZrHVzX+q6REWGToF3p+mrBwstrGaiU +j78gMgZnI2tiJ99HqmocjLXbwXxdvFmLkniBHAQ91D9fHfYL3odTEkjZ/Wi7DSZt +PAbEhbBIKpFKnbazyMB8ACeF+m7jQ/Nq1+g7ABEBAAG5AQ0EUYqnkAEIAOMb65Q/ +m/7+A8XBVNXgoyGDksgTDSIfvePi4saGdSJOcQWc2HbEHFhqcOqr50h0aOEBN+7W +B7X8ZMqqeCub1BlXLAs9gWhw5QK4mAKCtaARpmWj4sr22JscpMi4uutgcsfakRae +Tmd1J7efjcDNL0/EsQKQSGht1AakJN8zfyb6IBuuAqj3xegwFCu3MRanRyY8DP4C +YXfIyLBPLWfDVBViK0HOjEMEO7tdvsZ8h6ptLNqAYQQYcHFi75lKa626RVeEbxK3 +dptWfA7vQ59PlnsMhUZkiDDPzhnIatpfdBgA8EpBJ2Lrq7hE0nZI4/0rn/wxIPT9 +Ibl9JWeSfoT7Vc8AEQEAAYkBJQQYAQIADwUCUYqnkAIbDAUJBaOagAAKCRAQBmqX +BwkM+aaFB/9TyOSAJtb7qonJ8Q5Un4JLIhfTIaBj3cMHzP0ZbotpskDqnvRhUe9f +OM0KG+OibqaW8hKT3ZQVVIJeSVUCDYyrWX93KPV/tiL5F+5WWN+/L6Uvp6598oiA +ZvYBNNA5Gzn8kh4b5d68Qyr9W/2TAWm+jVYywlguNHsysLLTvPVG5OGK48/dYSeO +2PyCy7hpxV0z+xDinDDiVjwXc+vHoTg8ZDILQG83ZsICO7dO5FACCpFuKu2mjDvo +bcV4ajxKHNvnnNwFUxuGNogQOiMaeH62T/WBCdeyE8X0A+UsH6WVXmXkCSDxRD96 +UdK1vJJR/sRkoSFvf8E1KmFZR351/wZQiQElBBgBAgAPAhsMBQJX8oreBQkZM+ZJ +AAoJEBAGapcHCQz5ax8IAIMnwmPs5nvuo6A8mSEmKXzW2M9K4CimmR2HhLtFoo6n +HRcrL05I/XUq4YxqQLMmZeQ+N3lk5RVfZwJDIY3NzR/R0dGiRP4z6F1sz7qRPYxL +faFA8anPzrfW8tNtD9QeXhG9k2H4WVYdPwQe7zKmyf42XrJSX8bpgxAoRdGR6atr +VZFf4Dwgua+SdIrSpeiqXCqPVSh6xoQ6svNXwnee94zY8/R8I+6gqI7E0XaZY8ub +q4Ep950wDEkOIBB3HsTgUauXd9vdkFMnO20JWZRujDcE6U4GJsG6OsgllB2e0m9V +OL8rBTxpelg6gJCVVXLOLUbuZ9UUebjIiAABCA6V/9A= +=HrWq +-----END PGP PUBLIC KEY BLOCK----- + +pub 1188B69F6D6259CA +uid Chris Banes + +sub 0888B86856F9D71A +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF2hcBkBCAC2H5WcFoeByKBUAjRDjmP+5P6FRsZjLe6c1wy7G1ha6/EQUVK4 +gZUZYE9W7l/4QKHvAu4PvFWdD+5FXGZuB/2kw348CtabAlJTehm1QlPt5//ODkxB +fWPPz3uHBo5PQJZuLpStQn+aEkTTHK6Sk5033e6fa7mV5X/c8pTmTzkunG58NFbj +5VSVbks4pbafKoMY7rSN0/I2hEApCjB9tx/7DJuQ9gbaGhmabnhBTnwHXEV1/hIF +369lfiqeoeqDMOKj99C6KFD/NPRrRLfoPRpqL3LPsPp7P+TzyZN2q89Xqn2ysyI9 +jDtrlssEXLskU5kA8fVa179V/QR6QtrJ29m/ABEBAAG0HENocmlzIEJhbmVzIDxj +aHJpc0BiYW5lcy5tZT65AQ0EXaFwGQEIALaSv8Um4LBbyPM8PIfcSwl5sqBQal1n +w7461u8gGAi3uikn62Q7IGO3SRRuJcR6hkY5Dyq1qPwEPmcBjK0Nlx1NTtk4TwDT +ZsxexNV40qQ+BuZhCb0NmiQBRBY7o3GiFa8mqcNeTNnIxvqMYInIYcvHps3r77QN +Y8PZznfDNpJkuD/vvoFnDjncdaQd+Kia2gqAFHQMw6DnV1oCyPx38CqK10rw3DPt +yYo/sVo67mVLJZvoXx255DbKNATKSrhiuCIsK9r+lIvY9zXoI2cJFsNA1QSiDkt1 +yjW1us89+LMt5dVCanq0bfhprU4J3ZYqzUS1sVAekVY2jRw/ssidX+sAEQEAAYkB +PAQYAQgAJhYhBGS5sJ8WSqC/iHQuthGItp9tYlnKBQJdoXAZAhsMBQkDwmcAAAoJ +EBGItp9tYlnKg94H/2GsZGvWK8+dA4y0AmCtP4B6/WAUonGBMmUH1Ax8GaPKIQuC +ePBbQI5v2dGBqUOTjTh/qcpwfTHSMSWFRTv2HZDP82gY/tAHJrKZ14+Pr1JB4Nvd +PIj60dKFfyveC3t8TJ4kVjKvvDzGY7oHsZ1OmW1Nytsbe6Taj57UY8v67zpLRCEb +ZHIEo4hYtehEIlCWi7M2qJmXtWK/wU3bw0C8KL4Kun968+0005iOdQ/Iig4oI4K/ +UW2HkfQM1AwuwJlsc8uXUvH4A3kkVHTGnvvi4aTMtDWPKpdNu/eufWwuMrS6Gxdu +v7wjtN+wTGesv0OYCfxHScTi49XbBbU3PBq+pxQ= +=Lz+t +-----END PGP PUBLIC KEY BLOCK----- + +pub 1592D9DA6586CF26 +uid Aidan Follestad + +sub 63D8CA68A5C49F2F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=Hp6k +-----END PGP PUBLIC KEY BLOCK----- + +pub 15C71C0A4E0B8EDD +uid Matthias Bl?sing + +sub 891E4C2D471515FE +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +LDL7A4GNor2DcqTyOw3cjy0= +=pzVO +-----END PGP PUBLIC KEY BLOCK----- + +pub 1A2A1C94BDE89688 +sub A3F393B5D034A0A3 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +zTU22/559YQ9Bb9kRR9Rqg== +=GVBF +-----END PGP PUBLIC KEY BLOCK----- + +pub 1DA784CCB5C46DD5 +uid Rafael Winterhalter + +sub 7999BEFBA1039E8B +sub A7E989B0634097AC +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +pIXjPlQ0i6kV0h8KapE1Uo005JYgeg== +=ASmD +-----END PGP PUBLIC KEY BLOCK----- + +pub 1DE461528F1F1B2A +uid Julian Reschke (CODE SIGNING KEY) + +sub D4569BDF799A59AB +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +ZAstgdvEqFLHh1yX4I+if5VODD2nEzXFDzE= +=d5S6 +-----END PGP PUBLIC KEY BLOCK----- + +pub 1F7A8F87B9D8F501 +uid Download + +sub CFF46EE3C17E53E9 +sub C2148900BCD3C2AF +sub 7D1BE4480B61E2A7 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +nLx/8fai5lJq +=O9cg +-----END PGP PUBLIC KEY BLOCK----- + +pub 205C8673DC742C7C +uid Brian E Fox (CODE SIGNING KEY) + +sub AD9CEBA0521B1945 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +gMqQQpNoy+5xrNg+uv6po4Jjtidfww== +=sfdU +-----END PGP PUBLIC KEY BLOCK----- + +pub 2148325ADD28A1AD +sub 2D4537C5AFEEBF9B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFFCZsIBCADQOqjBEfLoBf7L+CCIqlJRZEMi81HgFJA1Gy3/4fXe9E3OtMAJ +dSkiQYPIzDqbNagYCpsMBSSduJ7EnXNPJoWHIAEn1q+FJcFMvTo4nIC5eKFu7eF9 +UBjF8MsmYKgvyPMWJuiY1/8SMDCKvGZA5x3LaPJfyF0CwUraYW8N5yaxyXP77Yhr +6keb+JMZ4Y092i5EpCykv1VhZm9zql6FRr6vVjKoLHBxaoO2kwBD2CAyDXGYBJAA +gmths3vGjVLgknKbYE3amsmfwxVL47ecuR23Wz/YE/7u/QtqNQo1FjljJ/qU4kkx ++TaaLMUtOkbrZU1SNsmA98CF7J5OUhVk6F55ABEBAAG5AQ0EUUJmwgEIAL3n3dDP +glovXzkoiEZQKPqnbH1WizgHrWHJ1c1vhhNpQW9gFteiMv7ss8C56IEfu50SRNmY +ZJwbYPnGPhe/okPv6BdgAN/H9tIRlLuY1dAxQkO3K/qbM+33sMDLEm/aZ4NwJlU9 +smKKiWy4B+9Rg2RiZXIRHl7Z2tFvb3BMJBZZmG2EYrH9AwpcIO0jfvF8rTkhuY7H +uPsPXUOgg0MpZdS5VtqC3OPKnKYcu2e2AUVfVUQDHw/7PQz3B2Nyx0POPDI/d2Od +ssNcBNqjadCCVmk+MVbVjBMv1AVxUhDWDDjobcMmjZSmQPSmbenxFeO1TUjxNAl/ +AuiGqkj5GhbxSNUAEQEAAYkBHwQYAQIACQUCUUJmwgIbDAAKCRAhSDJa3SihrXNf +B/9+N+OLoc6zdbWAdl9lWvgkl8MncpBQu33Lq4sC3nZ3OeKZm6CyI40iLwmD63RJ +qkU1g41P1pcitRfmEukX/eUJPM3dDLkE9NGYU/H252FZGi5oOXEL3rWHnewykTVA +EVS0j2GgiMTePyaYA8AsI7ZAXv/FnAzlYaPJkI3dDyYzEnx+Eez6cV3GxWLhnD+6 +QiUlHKjFvZYJ4p7GbvXW9+y9eS4bVjsb9oe8/rVSuNC0blqpbZrvDoVHXbingv3d ++OL0uIIop5lk6dsCW8c7dJRpcgWzzdRAhLs+XVc62Y2VLRIMcID6agvA44qAijzq +KL63dnDfxqDAN5pdHJbzl7oE +=knbd +-----END PGP PUBLIC KEY BLOCK----- + +pub 21939FF0CA2A6567 +uid Alex Herbert (CODE SIGNING KEY) + +sub A98BD25BE464EA45 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +NblrOf6LII4Pqx0ip7s95q54JSpekQZ65g== +=3q/e +-----END PGP PUBLIC KEY BLOCK----- + +pub 21A24B3F8B0F594A +sub D658968EFD5E9F85 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +4PN9IC8HIL+bVVb6xuqwAD3bxGc3bYxA +=9XqP +-----END PGP PUBLIC KEY BLOCK----- + +pub 22E44AC0622B91C3 +sub BFE9E301CD277BAF +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFpqN94BCACaAb8Afmng1QPu5k5uzLoA1FJnF6Wf31ZU1FzDxHFHLNUYSWN2 +Bg6k95QH5ruZ+Z/QOJSoIB+b3htDklyxd8m+G2KsMIqnQs0BaTN18hb3PFyMIknM +YWkkTPF3nVV9APk73AebTAcd2V1GB7xOP+L3T7tyUcB9/7bDeM3od6qPksdVGNMA +0S32U8SNhe4jw8uXKdFL6PSxgyg9yeu0V7DyR92V9jF+ZicZWxaLeKpf/Vn3MBX8 +JdePR9SCJc8CNj0n/tsvg/aSmGZ3OMZTUYYvrtfgpXUw0WVkyma+T0ANcdDN91uZ +P8lV3o+Ic8f15xwsTePDhMhmtOapIz/85ukFABEBAAG5AQ0EWmo33gEIAMB9fJ0T +VVhqKzqj/gmlVDCT0kvevaGSDB83rwHatG/D2h9dmipoEIWBvD42/PXkYuY42iIO +8/itvVOxpPZOL+FNRvei/ZbVEno4VGaJKQ646NkeVWyVgXZ8+VkRdZ0n09a+goz/ +e1pogJfL8BVKbU0F6trWXYywnV4+vp2kwwMGNRTXmvNabdY6rAE0TfjCGE6O9T28 +OXy7iHXFX3oTkHjtltUHWlHrLe9JhCDCPoFiSndTPYyDcZD+cEWg27BX8XsuJRvl +brzA57xD3w25ESHWJyj84z6K52M/Ys7S/PawZ7pflRkbAJ9smeWr9+qg+GLJboc9 +vX4kdTTyQp5jkEkAEQEAAYkBNgQYAQgAIBYhBMcLhE8ALyH20rnIdSLkSsBiK5HD +BQJaajfeAhsMAAoJECLkSsBiK5HD5coH/ROhc7Z1pjr9mWR8rr07yNEHNuGf3T/Z +148z+ovLkV7dRobayB64VhtMwZtE+8Kmf0Tltx5zN2GQkqoSPl08dWFRTr6N7If8 +N0OCtw/XZJBHa8eD2FF7SnNWi6IwNfhCdRtbT0LCmaHKaUineldvmM1riI3GNggG +a4cMSYw/65blR757aWAgAVZEWi9n0LUMKDRhjIaS1zjtxqSrcQ0o1TYsW9FxuCjJ ++MzQSmtJQKiVRQ03fLJQ1z4j5u8YvmzCEqp2dEdqBuDuR6Wyf/TmgMUY9AeLT9U6 +VZTvAGH69TDqVvdmLsKhtQidSFbes+7ku5tM61P6ggfv5FJQgQk7WEQ= +=k4Pb +-----END PGP PUBLIC KEY BLOCK----- + +pub 23778689FBFBE047 +sub 40D34E692FCBA56E +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZXgxpxYJKwYBBAHaRw8BAQdAbIycVyFgMJnhB7aqYPz+dU9MIlsok/qclCuI +IsilKgS4OARleDGnEgorBgEEAZdVAQUBAQdA3w+8KkqAyic6PWfjlTbhe1upf6Rr +Qt0Dqns3NoQYoU0DAQgHiHgEGBYKACAWIQSxcTbQYVNVgQHbffcjd4aJ+/vgRwUC +ZXgxpwIbDAAKCRAjd4aJ+/vgR/CkAQD79gNi5NkEtK8jqYIYc4HJyHx8LuNov7Wh +A5u9ZhIgTwEAhjiQCoCjYE/F9u0Y8fQrlalxahFEfM+qf31PB6fF7Qw= +=60bi +-----END PGP PUBLIC KEY BLOCK----- + +pub 280D66A55F5316C5 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFOOGVgBCACiDwUZOc6943aBGUrxikkfUnsyZfHtF9jihYmA1pSgfsye+JxR +oG9QWW9+3qx4L/d4ZEqBftTWpsjyrY7NyMaeXtJEjE0vhiWNehgXB1z4XTJ66zCX +nhlMvixGLQtfZANqCxOmtUGoSXw+oRFY/SExAioSS19HlSxApSaUzc0prdujqp9k +vOKKIBWTBIUELdDTA4+enfzkAnIINUX9LcMTmO+Fh0AvfjDbq4fr8rBglyVUSCqt +TOT4oGZlbpsq9TOKrTXh5go0rm5KJcbgKvX78ZErK6pcpTgNA+XFXCz1rQ9nkIQt +HxWaEMJtpSkIvHIBz9qoAroGtNFzz2oF4ElRABEBAAE= +=1QGy +-----END PGP PUBLIC KEY BLOCK----- + +pub 29579F18FA8FD93B +uid Tom Ball + +sub 9DF7F2349731D55B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFYFiMABCADYpblWssqGxbjTwsyroPh48BwdSKl59zbFKoEHDw87NeWq7fik +h95RkbdeWsQSvduXWgQZsUDq9cLOkuS/ChAMkAAd3MPp1NMdFmAqS7BX5wU5s5I7 +XD+/p51SWLMvgrLxoenmoE04EuQqQiXd4DbU+HGPseiNx+mN0cxPssaZMBBsmi2r +RjwcQrFTaC1iffzh8FKLQvoTDzci//b5bWcxCLbsY9dYcUaDCbBAkL8HzyZUKNE9 +XwXh/Rq8wDakI/VEg/905a9c4xq6Rss6Yn5E4V2SAo2+B3hYmvHFsefaM9kkqvXk +MQ6zjx83LAtzavOzmthjhhPIgCAfoQ5Q5oDzABEBAAG0HVRvbSBCYWxsIDx0YmFs +bDcyNEBnbWFpbC5jb20+uQENBFYFiMABCACdvSlhh2hLe4F1fBMHiZK2HdNp8I3N +S8o1E9k0cBM2fTfalIlan6ZIJ2Z+JqRwk6MRaKpB2or/0A34+3KfI22SWtsI2lJA +2x9qWaiwRidLFFAWdSjTzNroYVkcwJ5hf2yKN/mH5YRiDSzaqHr5GKKPXHXpT94X +qXn+Pj1Lj5ipnqPOerpJ5mlkPPSz8C5Ve6I+sIqjGKdtrB6kxgIF3kf30izCu3dL +0j5vuey2XneRAqETHqmBVMEjFeuEY0zJCj7LxQRr0YaaSfrlkIjIxRbhatgxXjQV +bbnh0nYAh8dUz/YvsfuyZmLJhRZkcJxHW8Tt0xxV2oBUBq+IpmvqDoy3ABEBAAGJ +AR8EGAECAAkFAlYFiMACGwwACgkQKVefGPqP2TvmWQgAi9Q5WlckTYzccwvt9F+s +RspD8AncDERdwkY6HiInLLNqQSUWiHU2BaYN2wmSiSeSgwurPtN85nd3XZyhKtXx +H0XKC2fTzQWBdyBEh5zT5UevES9nIzIKurHoG4TsWr9d2XDiDp8q1s4G1cNGYDfD +97wpZRbYn8L7hedL07ISEWNdRvBpbrvhme7X0pD8MBKPqUXfLHHaJetmlFKOmu9a +wzjINlz1C89JdocSln3lLJLE+RGNN7HmXdwmKjmnC0fo2h8jnOTYekdTM3Ec+uWE +8M1WyqZV7WYzoITIfq0uHgyIxCpaOOejOLKShQW031G/kEvZApAVPHLFM8BdJ5P6 +Dw== +=INDk +-----END PGP PUBLIC KEY BLOCK----- + +pub 296CD27F60EED12C +sub D95ECEC170500D9F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBFsHC7gBDADlkoJglNVbX9MShcAm6jvS5atCZwWT63gSasObXFxswsJQd1NK +qryHNcj9tKBfLbSpMOoHeyyIKDdwdxN+6+N9Hi4hf0j1Ub6deJyI8ace8VERWaxF +oWE2hKVLuY6GzlNEve421WJSThDtG3Y1jcCB8sQ9NLEhzB8Qh/eoqBP5IGNMM+XP +XsMDIg+15sqMpEN3oTb0WUNNaAoiWVaRJAYbQG6DsqGSBZQEo1o7K4o8xrIP3Hft +aHn0eaQqPSxK/D0bLLDaeRxxo4u8lefVSy1dYW/70A5+kZKbHkR95zUU+GoSHBIC +9hh+U9pcdf8Q1iDiN/BAuMtYBqG6I61UZDqaEUsxrR3iTa2RpHpclbqb7kED5kFH +ggaXMBP3w2PLZ7iZAOd6eBPP3T0pOMDnNduAecFC34vYgPqXeN/0wV1VQWAc1FlB +l8e10i4fcrCCq2YO9up55M3ZiX0OINabpZsPfTj11C9n6olTR0TiTsHiJKViL+Jr +tAscFDboH3HXC1cAEQEAAbkBjQRbBwu4AQwA2w0BcLAcrBNFxYaqgR/u+I8OZkLR +w9ArcDm4SQHJ+JSODDpmCyb7gOUs24Nx0P3za6dag4TLGXXcDfv7TgFlSzeUcvz0 +whyAWfJMkuXs5+BlFe8+puDbLadcj9IfwWQfct8N8MjAiRxduGCAKQHqSD+raepP +NaC0NPEvGXYaYCT9MzDOJtMFnxVxwhhmSBNQjm4kOWbnwdZVdP2qkBQ2XxVy+/nD +bOCzno/chjBla2pgBAN4Wi9nmUGdTiFN6gOlAOb5awaKWz5KsDwCGkgoXwoA7/pA +cUT7MaRcoOyr/VnAyIq3jAMXkLmm9VUlOJh1oemY6KohREJU4yMMcoqsS6Pd9ici +i2iHMcbLyC+RH/Z7scPWRq8ylWuD78n7kORCf10m+Ey7CpBwb29cDNIzBYiwNBOU +F1L+m9UuSX0XIy43/YbcXTStZaPO+3t3422YWKSxkjF93cIQ8zrel6b51SzqFhKs +1VfY1P929S6qW92C1sqAeA12PtJVg/XBJyrvABEBAAGJAbwEGAEKACYWIQRL95uC +WQB7Vm0vzoIpbNJ/YO7RLAUCWwcLuAIbDAUJCWYBgAAKCRApbNJ/YO7RLOm/DADU +L72DZSb/0ZXeAnyaT/Op59qaG9KxpKbPXYEaYto0AhEMDWEeAN4nHxsl/nNJEG1n +f+qdDtrVhd6E2ORFv4Y0LIrDNN7vp5mUo4Stsbn6AXL+UYtqS9ChWCLnds8dfOJT +q6xOr9XKbWoIoqGWxFfjrYFEYdQ5vbdUfj6xpy2dS5b2bBkLoRpfsAz4ViDfZC+u +zO9uHhsI9C/YzrO7KqaMB4aHL2iB/Na5c+VuT39NZ/PhLvnYSJ0DgpBg+EMEKXS3 +d7+wTZbIeAEMQsB5w1SFoGm/eUlTnitvot2rIn+zzkKBfetYFqrxFM8YnP30R6KL +BVJeTR9siRFdVUOcvCheWCt3nT9l9JJNP1ceUe7e8TrcBC4qNvEPE6ZRQi6kD4fd +C6dzM2X77CDLsmuKMcSqg388wfg286OSdKsoCgj1YDvUQqWe61UbjRPE7NArAK2g +x/bMv9iz1kdOKxHCq3agJjuBOAF0H5MI/eTLpbySzrh5PAD5/2W6CzkFMpH6a6E= +=A7+U +-----END PGP PUBLIC KEY BLOCK----- + +pub 2C7B12F2A511E325 +sub 10DA72CD7FBFA159 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE+ZO+EBCAC3fZOOuYKthr0GcUge0PH2bh18sbM9XUmPKQz/W15l1NA/2ARS +2gUXM0R+SunMlun9KsqjnojJ2ObVPvbm1Hg/66JSRgR3JWfIpSlJxLicpfu8rCfN +bOjh4v9ZipD+px8w3o/RNrnZH/KRsoJg9yER6pf+pUZqTJfdg5lXezc1WF+/1qVo +ypldMGfrkfLsPrUZTT689ubbig978e7eYmJEqldtaIwaAzHQnB70wIJyg/rEwFUM +ldsvs6t6czSuJ4zPMvmh8TMpTg9e6+DMktPl1CWRONl8RPpgYMIC96gb4OnfDDjk +Ex6clSCwgbDwdeAyOjjR6pVq+pCNTo1Pcj5jABEBAAG5AQ0ET5k74QEIALaxogmJ +1t7arw82krV7ILlcOu6aLuuXTuy03K1/jU73oyWfUqwvPSbH4igcLb8kt1/6ogfk +u0T9tTx+0mDsvqK8A8NZ2nDTXok3T58UAg0FTXlqUqZmy5QPtG+it2j3/xGgip3V +5p0Ml1TqEl2SfW6gHtLptDUWzuzPi9SgK1ZFlueprPg7xwHmWhp0gwx0KSSOYWlK +oEllj/1aDxFNcdKogWcGN5aJEsETCEguBP7olL75u6732wc3zola4zTy5bFT4kEY +Dk30Du3VGQJrdsqlibdQpZYm8uH4AVXDmFMdEAjIs/DGRrUgde/oUqwtgm9U+p9M +qcbmMoeLFdi7ajEAEQEAAYkBNgQYAQIACQUCT5k74QIbDAAhCRAsexLypRHjJRYh +BEdfO45Z5uY6p4BnSCx7EvKlEeMlX0UIAKS+4ZAKrGG9jbWfzTTDbu9zzkXgV13s +uMD+XcGz10DkdluTUBXj8wWlp289fXNm4E49ipsNK+dcZ+gOATjUvb1Llh6D6bHz +1QM7olxBCeU2feTmYYKBH8GYY9JZzfAXNMQhcNiiPj+ntZqePy/EFA4uZHM7We7v +l2c7CBcDAq1NNeEczo0KvG7AWt6QoaMVmbvA14EKadNzrmEy9apkag1BKvwzXInY +CvIHMa9ZqicOSUcI5QCYu5TufvIE7Eq3Khh2Ex1FiOaEA+57LMrt6NsSKXrB8JNY +bI5pqE1rxJXZnYtx3ZpPAAEfLjPdi1AOkWhvhsoPmiGFC6ebYQ5eVbI= +=+5D1 +-----END PGP PUBLIC KEY BLOCK----- + +pub 2D0E1FB8FE4B68B4 +uid Joakim Erdfelt + +sub FCF74AFDF5947ABA +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +D78JpXJRgHL1vib++Q== +=dGtv +-----END PGP PUBLIC KEY BLOCK----- + +pub 2F87304808D40CEC +uid Nick Parker + +sub 140DDAD618A0610F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +RT2Fl7A= +=E6L7 +-----END PGP PUBLIC KEY BLOCK----- + +pub 31D2D79DF7E85DD3 +uid Markus KARG + +sub D091C8FFA534EDA2 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGTNOPIBDACjeIqMmK4jo8NBVVacAwmqnL6H2/ixU/rPg1WEJSJRQbWu0otK +Zrs+0tOVuYsQReW5tYUwI8hclSkdO95NC8bM7rlxcO6JSixsjzf2cOXajOAvuLMj +OlGtMPblTI88/2nPmj+k3jLClsuqdXNIPdf6DQGIrJjsNfpnCO184ialqIVv0X4y +Z3e8aR/coToU+CcgbWXEyqqZL35RGLs/DR1o0supIm6tBEaDGGxRcrOGTkKaQEXV +v7cZrrXQBSU1kOLko2Lz95ipUn2s7BxM66joOSzmeS6QDRqoL6B+PYMEOGqOP9AI +ZgFdMh5T4WJdzuDGOBrX1FAD1ZLeumz5+Td7Tw/7odKH10ToFf8DIvP0CJnAC8EK +4HGl3NYyKv0gafEmprVNSIDrGLjY7D6bvlJ0vJ70gsQAhJF/tGfa7+pBA3/zuLe7 +WD2pSyiqdsyl2mZZ/KRtMXDrhvzSk3H0nxZ4+BQhNLVBW62ty/+XYWn6QOw6tD2v +4vZCXePrel4RhjEAEQEAAbQkTWFya3VzIEtBUkcgPG1hcmt1c0BoZWFkY3Jhc2hp +bmcuZXU+uQGNBGTNOPIBDAD1VVbxdqn7crMimZKLrwWyNVGGu/YKzrpKyO6h40m3 +TCUYslIDECFHQL2LqfzixL9w/nXn5Oqx4rTRnlfjCZJkJUv4OnV1dIW065T+bepL +srbZ5cNnMin2336ClJwnD0fkyjrVSAI59roS04WELHvkKyl3LeaqgbJMYEMoweH3 +LrrKXGi3fefdEBevRR80ulpw7o0AhPhjd5NWsp/1P43Xl9poNoWaL+7/7W1jKKIa +7VYEOWbRZ/mngwLbSqLOc0lHCsH2InL8+GpdTUCeUtBrImQItKneaxkk+qn8i91d +zL7/Mp3LI7z5ajaXhSj2Wf6/0rg8ObZV9sBJtkEVp0oq7+mFmgeF6reefyKX7g+r +nA1gkdXgMPOBqExXFsXKNLU3mtRd1KsNgdQYDPn9enncmKzjW4ha7NcaFrSWchXE +73Fq43dMbCOejVy4io6+BBSXXHe8M/3/ZB+4EFohRPW0kODnngxzE3E9usuvRJTR +t4c9hhJZdMuhhPs3ry1Cql0AEQEAAYkBvAQYAQoAJhYhBB2FRp2FWcLh31+SUTHS +15336F3TBQJkzTjyAhsMBQkDwmcAAAoJEDHS15336F3T9kMMAIJHtFRvBMfCh9p0 +UM4jf0ahE1LX+fUhF4zf/t+vlUmRsIHJNcteD04SxG1WRDc5xxO0IO9oAnX0AgC9 +NOCAZaJ11TlXPsCRk32+x8bfCe2Wf2hOLPOfZGO+vj89m74iJf0Gh/0BySMvgwcn +K6WL/xb61EGQUxetB3jLpZU9FwQZq6LjftvZi7r6Y3ui3ogUwTmvq0HlsKCmY7GM +1JLN6xAChZbDU+Ox2F46wyo4XWLfX7HtTe6ykr+yxDwTxx3mJUCwbBYr78Xy16Rq +COcemMTU0U9OWFA1JghFul0moaW+gV+VtZpWR1nCFn5xJOzD2UWFFWomYWiSvL6O +qEVfV/jQYAREDj9vpxlBcfRpTiUlrdFQf4uOQT1G/nXY1KITmq/hNqUdhpUjCoMC +OvdyVwwfG12cTWeQiDQofGqJSptQmsUvegmfF5byUPDbgjCi4tEpWOncOeehs/Hd +ZgMBzI+v/ingWOyKpKqhpZR/50PHA0o23zw8P1BGeQOlr4kFNA== +=jR+6 +-----END PGP PUBLIC KEY BLOCK----- + +pub 34918B7D3969D2F5 +sub 5CE9BCD2ED28F793 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBF0vfHYBDADEDPY9ub98c7jQe4yMbPke3A/sxNHnn0WuA9JN880DPs3L7lrv +9VHTOlFXslDNBPYSbgFXH5YlMGg8ZY8bhngjc+Z3dtrCX1cAjUXOnibi7fBFomLB +xvKzTHyWprguV6B2YAldKpqA4DtecJEF6jusNPptSpMN2olZGcxVrTB1s75eO5Lr +MRIvZoWxvkH76KxisytDh/Z3MJMi9fFD+2OMsC/WynOs0TIih1T5U2jCz25dwkez +zb3Bd4G6E85fS+weJPXMRiezimF8WyFN+dDrFgpwWqgA24jbKG/tfF6sAuvGmPgw +aKIv2VFrdstqfCVC9p2nzuchIDS85f/D+fEjBsSj8spUzA15rD0T1/9BHxtW+L92 +fcTs0rTGT4sP5HPl2aD9R/NP03Ywg8bDqcBWofTuCMtfDz5lUBpeOPngByDiKtQC +tpsB0PyhPoMkrn701QSkMXPO6yLP6VZH5f9qhpvWrHLqsd4GEA+PoRuBJbYoDErq +5hLW0Sgi9qDyzLMAEQEAAbkBjQRdL3x2AQwA0AiGxJazb4NpJvRf8WDb4/SUJjnC +XFIgAyXGKiSo8DzLT2Wr+GDt7ehPKu93EBLpgyDja5eGZ1UyG+CgRBbyrMZW6Dsa +kSScaD7eEfKiz0lCaiQ0aGjDKrHGsUb+ik0t4aQSeoGuLYuRdnTr6ockRFgl34xI +VXKwTp+J2nqDzX82Uz0CyhqdkRyv5EU7lWy7FbuhKdTMj7rY+LNTQbOzXtyU7sRJ +Dj9ZqxuoVNnjWTM+6KFB7pWrIJw2b6uqq2CqAT8sZu1craTwevo1vCbI7yWRQdSq +EGTPN7pYhjSynSGOtgDXN5/ohKdMEGcyLJIIz7wpVyoKmws6gGEos/TZeeOkqQoZ +75WAymC9n47KPXNCYtysIunCxJgNx6rSFU+IWgTsuD21Wlu1utrbHMAngxhQyhfs +9F+D+UIZr5py9DeFE73I55i1YWUAxC8aNIGKy07MTshYhoskxkNrRTDAVzOevqC5 +33qDLFEa3CwHxK/ulHip9HVmsw1wN4tGLy6fABEBAAGJAbwEGAEKACYWIQRPj+xn +hfYR2acS6ic0kYt9OWnS9QUCXS98dgIbDAUJA8JnAAAKCRA0kYt9OWnS9efzC/4s +98rrGn4bBuM4dSiYVbskCg4p5kXqAX60wbhE7tsun8G9asAqceOeJk+2kYLRWhEB +7TUFvu60Iw8J/AjILtsT+NiNuhNMZQH7Ob3LLSd1nTq3G3yBRJ0q0FhFLpl+H1qD +K9ZtrjgtXMvLknRk0T4wkrzk8ZphlgW4AHr4Z4dz/uHoG9WgRIH78+63JJaHcan7 +SXCpZx3oalnUvPksjNc/r4typOEnMzDIAoFDW92SY5i9BdZABwF5aJcYWY4jg6Af +lqchx18PGoRoqi8/ac+gy1RnunpdMSvRn+ig4OY3wbw9N6fUQajSnPoGGeaE1KGE +CC5h5sfS7z3VPQmJiHoP3rA3CuIXdPhiathsyTSGZKAUDP24uca6R4fWSZ8FqwCa +u8yHMcSVjxSuM4ab2RpQmPf/XZwopx/nqVdHeDzm+0lHws/l36zmHU2LPxuVAoIA +lawcQqnavcHSKD4ItfKVKitKy12yHv9ENTJC45yfTKnJxhj0jVeXdn/0PWPFPRA= +=n1rX +-----END PGP PUBLIC KEY BLOCK----- + +pub 36D4E9618F3ADAB5 +sub C4935FA8AC763C70 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGGiftwBDAC94Yhhh/5yO8jYFkg01MPnooXKZEPwxAbAg9wn5iM0tHxhEpkU +zJVYZ+JYq013+Ldp8Of7A/d6hKTtZ0xwSeY7S/WFykIk6tc0P5j0sfFS3pGPDk+W +D3DwUa+8m0PriF7iA57vCOE51znO/IUIA3PG2YAK6jv2/i8MDXOOq3qB7VrbvKGB +kIPubp5PbjvP+LFhLuUReU9m2y/3q9lNFXdd9kE2iScqGmu3FDhRJxBK/WQ2kqiv +sJZjAYeHEVNcc88Ah6vXI73uYrvWVGCErzswYy9UrxCAQ/x2OxUdLw7NTHwjZSYC +JvH5JPPTlDxMgfwTIsmaECtw4QgiVmvDp+RVa9zyrdI++RNr0InsXv9gWMv3p3yf +TF20ZL8znFYVUi6XkeQhZjT4fHwDqDVnxhSAFe3E0cwHFJBQe2EFLljwNy6VYnio +wBr7HrAxczRRqlUy4a3bH5KwiNwwvxgqfdMj9KTVpP9t98/TA36bIohwGFRWB7W4 +i395S90NsTbCh/cAEQEAAbkBjQRhon7cAQwAtPmKcM1/z8sMJnt4sHe3ndXsOdSq +TJbRkAcdyDO1F4qgj5z9wkrlVVKGuVtmJS3qmR901Q+oH+JqM6UeGqhNig4IQvME +iQjjelvKXMX9PPVzlP+ga5Y1/2mnUmgmYXK406CU7aaQ3hs7++XDonnQUt5nWF9d +XT+xK/SDLYMk5i1TNqPVFZBPm44HpIjKGNJXD7Vv/5z62+hKswpLXgYt8Rz95ByG +ncjQ1Lo2M1T1Y/EuwlRoc9RTdyABavSQWVLKIz6kKM4LejajjRvLnybMUug0CJl5 +mni4cHXx9t0pMlG5DE2O3mZLwTgWcJ8cu2CtPxA9iLfVvFAThxk3ZitkEhChBtG9 +/V8D4DiTIht6bd49xkHP5pxtB/fuo9lNb0axSBaOAeant3KA6F6vki+chnGhOFqV +1KJHcxYG7VsG1hYhy5IbZsg4GdcXfTwwF1/mq8kvHfyTkBy6HMDGwpr0ATNnrxO7 +tJTiVqDuxfviGQUjqJIQDns6fM9BI4OfpXyjABEBAAGJAbYEGAEIACAWIQRH62g2 +JF0tQOid+0E21OlhjzratQUCYaJ+3AIbDAAKCRA21OlhjzratRBcDACCfhsaCFvM +JTls5lT/dcTqSCYJYZyDj95DlTiaRNkXnAGrTyE45PnmJLv6FFZzSZdu/eLE8ls2 +MY/KWqnZYYV2Mct/pwDDLSjdAN/NSRe9HeAh2OS1kNeN2SIcoL55gEodKBNSMisY +9N3ylLMxHZPa5LNBo+j9wftEaVi2fTX8LDJFQvUOZ3f7cz3f6u42FeHUqaLm2alH +QSkfTB3yIu8Hmo2EXHh4UfwTmS55OBGLQ91d8neu7PcimqCeadeHW+qY5g5hr5NN +LxMA+n+vwPGcQNxg0lH2XBdlFBbAELEGxMcKbW51QL2h+EdwGzT/nK3Iia/qm3N5 +0Z12j/hhzohlf7TQjhzB43Wbxef94JbKacvng8t/hG3+n8UASQzizCSn/oMkXQom +XdQe5JFgJCroU2CfrdFmZfbkkq9mAi80BLUEAGNTUQrg/W39VX1/klGiXiWtpU8g +q/tSGRQHXTwG53qejlUtKI315ZizOhJiniSDx4fZaK2zB9RlZkrnd1Y= +=KPJF +-----END PGP PUBLIC KEY BLOCK----- + +pub 379CE192D401AB61 +sub 0CFE993CDBE1D0A2 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFTi8JIBEACcN1ucQ1uCOZ1owTELQV/6i4q7NbYdJ5wf7yPYfEugSo3yfbo3 +Pw/XEvlnpDZmT155sGNOkteZtZMdcm5XhFbdtquLlrkjAcUGatq5rAt3eLAlvU7u +CBCDJg3ZaqpZti5ti2TfiaXHeawTpxaTb3V5tT4NYhY0aJqe0MGoVl2yZyoKMWsL +8XcUiJkUYnpu98BvnzO9ORSnKWHk60YxzZuHh5buMNiV4aI331ogiTxqISzTwEdQ +ygtlp4IeqE6w4x4RUOqQg/mu0xhqnP375KksPtKALLEr9vgqsJXfWVa5UmNl+rZP +gMiNEt+Abwewa6IQGgSU8GuxMp3qHxZtJQRNwIPx/yb7FngtWrUKIoQXs9xJwdJB +z4vhfFVeQlyPkEycQNcRfHVzK62oF8L5Jj/D8BIGAD+dj3x10Cy+qVK6BTY/F1zv +5iL12LjSlz8DtmTbqjit0WGoULjXFZALAU36q6FmE/nMcFuLaTUIinGV4fMvLgf9 +Zn44juAhZMweOt63Pn4n/K0W+uOdrLSmGxJDhoxztabUdIpIMsw44wZ8gnSmPAef +IDTCjJO2x9s2YuaZbgstpJldooxGJ+FTe52QXFphti+tkiGOg6Tpj8Xq3+ZEM3L9 +Js38SSdys0XBCHYiCv3/4Fk4jspTsCFrDzJ9HqNjsiktxPm9szmUZ72RjwARAQAB +uQINBFTi8JIBEACq+dSR6serUWrem1itiw0MslItsFyHuOV0+K8ZUOLRge/arBSf +Gjk7YZPFzIMVbxXo7LYiciHCydZ9K7HdqCqygC4k2IV+85Ll07ZfraPHa2vfgXsh +u03+VZcMcp6Jxs+UPlVHV7SE2R3o2w+KvKqzLLRLb6aBREoJCsI60HTWyPjsHiHr +aJ+XFNl0LT22tIPJFjOTeVKU/8OMBs3O5ql3zgdMG3DFGAS2ALiCb1wh+YgJ9c8T +A44R52Jp0z1XUYXvV298FzHD6n7ejwif2MNUkLF7oFfSknQLkAw1WuqkwYn3QYoc +fp8aW5u3139vWWR5V2yLWeGI1+/spTJqP8eXBnF+jPWuig/GkHGrWCn+MT7Xv8TT +2wR4rdhetkYPnPNX0ra+jURZbie6tO/C5OWTYjurTSzBDiPxNLcxxUNjrOMzIbcL +LhSRQ0DTFLiC56D+5UvPIUY/GiX5O7x4iF1kwSPcoXz1w+xzzCwfFZg9oE5voHAy +brGkTFCIb5Oo+WKWDCY56K7yHLIUT4UmiF2Liaz7gesTc5yFSFJhP0WpkVX6FxDu +oCryQx0L38qD+4c445N7aUfVmqbOBBp4ORpJ/w0s8Rb946yQ8TTUB06otovyIz1i +Zsuj0yU9kzZYovrZpKJLeDEY2ThxdU/O3ZkAowEeTjW+KyddTT9rUuggAwARAQAB +iQI2BBgBAgAJBQJU4vCSAhsMACEJEDec4ZLUAathFiEEh1bE92XJrDy2uF1iN5zh +ktQBq2HMkhAAmSHv8by4xD4YlbVKnAAj8ly+FO7gPleTm2VUZiipZLqKtwEBkLPy +wMgkmCBkcNd4QJ9V+rZ4q3k7/rxKDA2l9LAdalMOPVjrgfIe9TWhf1GGj3oY0Vsj +TauVJfonDcHPCPLYH5FFBQpv2uZyYfKWVn6PuNOLoxCcyCNkJOkqGaoW6tsa04Aq +6yh8RuRqfmt+WNr/kwvDpADdxrvSwYN/bdOfMQKwYpJPCI53gmyKQ1eRb+TvSdRr +hRBxR9jSKJWhqaRDrMHdAC7N8zpkPCjmcS2uBp5+fYJEOv/A29c101tUFEFospbx +bXtKvn3EsIShICSssjdKbZOGNu63JWcSzLzwI1tfW6p1UbYoI1hyZcZgc19m6HS1 +m8dXkYkuUCHjpN9gmZUDexBNJZ4ii1TR0XgqwZogVUR/ZLChWxWn7NrCPexT9kMk +kLroHDN9Kg7rQNgqKOfEqAGAGDPtCZSksMBUR9JhgzwosMUszruATZSKMUIdxM67 +JosogeIVj5/as1+VTNiF77S9Vs7/K1LaQDZLKPTD47DAH9WPygQWlJtEHoaDMAlh +f9SsLBfRDbusaJ+Jfhn5D4bgrBBxHaliFRl6wy0rITBmex3cgLVBz11xVdBQk9yq +leFIia4wM7+46n0klLF0/EH+B/oQvJU30gWxTX8T9eE/hCREmtkjUX8= +=rlEj +-----END PGP PUBLIC KEY BLOCK----- + +pub 38EE757D69184620 +uid Lasse Collin + +sub 5923A9D358ADF744 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +5KWdK5M= +=H/sd +-----END PGP PUBLIC KEY BLOCK----- + +pub 3A1959EEF8726006 +uid Eclipse Project for JAF + +sub D908A43FB7EC07AC +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +x8rPAFrvagaS2grj0z9tIo3Ki5HXDlWO9Q== +=rPkh +-----END PGP PUBLIC KEY BLOCK----- + +pub 3AD93C3C677A106E +uid Carl Mastrangelo + +sub 9B2A1B698A113AAD +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=qc4l +-----END PGP PUBLIC KEY BLOCK----- + +pub 3C27D97B0C83A85C +uid Alan Malloy + +sub 4BC7B9A81C39EBA0 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGEdX1MBDACuRDzoPMh3CyUHQydFo363R6OdXqMZ8mJQMdysIJCXOXZGRwUC +uyPOUfH6uSG24RU2zvD72D2SGAehQKLXLQeN6XCt9PRAszP18dJADm10xgkXJm+G +GJm69bRYP0GIskQI0D2hXoUlSyXFKZa154pkVzmeM40UXo90FrMC/YjH5dLp7uDn +QtZbsASlHiy7lXFX0IoJHSHZFppmHcW2KOmFfKwgE9dpku7CdTdySY9BXiLC/Erb +l2WjwzSDEkQbnq6Jm3/wb/AXxDEu9H2SE6kOxrERqXBfc1ycaEsJMxpLxYpk/kGz +U6YXqXiOla1SYC78/SnSV8Dkj4/hN1/XtFmkmLUn/WgctmPnsE+fMN/ALXrH2OE7 +pUYLTy7jxJ46dChpjIPJ6Tp5z7EbxdsXR9JwLFQP+Fyp/anqLO/uLkZhZGhI3r6t +lvFyZW8zoAuf6UpKL6bIvxld9SDuEqahbU1RcLsK/7Lwh8gFYXvq6k9siV2Fs4K+ +UWyVrn5cdSMErMMAEQEAAbQgQWxhbiBNYWxsb3kgPGFtYWxsb3lAZ29vZ2xlLmNv +bT65AY0EYR1fUwEMANMwR7AK2pVja2QXQpFx7zSNFaniXyMdXOgDKhVuhTe99ed7 +mp+gGJKXgUls/Yh4HIWWOVF4GisMe+XlvvPV+F9EgVNi2wd/efOPlQ3Y4nsp61uB +x9hb/FSYnIqMjGMgf9ehEYnZCD0QERDdyukrg10ZJMyGRS2x4UWWelwcig13gGf1 +Oekz4LL2BLHRaI+75/uOGT9RmgHo3ub+56l4hwdZTa6RcE+zVe2EF0qZKkCeAA2i ++I8xh0u7pho2Of0+HcvuN6kzyKw0mpBxRhJXzX1ucuOrWiYgPh7+AaMCC4wzB7JG +vYVzNdtwgrlEvGUGJqLmJXikwnH6d+McV7ikTQyYGPKIgbI16JHQqGkYXXEvz65U +L1nwgPw+PVyxgnl3Qub3jnhiRfVuX5Oar/Aatbw/452qR1H/dlZAifmlasp39KT0 +yq3LX9HzbLbbLRqz0hoPkVujZ0NtAeM4KsGQGDBPs/cNs45pi45u/AUSfp81b4JZ +fGh8uW3peejnUpXSuwARAQABiQG8BBgBCgAmFiEEb2Vrf2v7I404rPgfPCfZewyD +qFwFAmEdX1MCGwwFCQPCZwAACgkQPCfZewyDqFwcewwAraeFuG7lc0CmC2TNBcbn +G/AIMycqzFP9NZ+MrP4RwSzZQh4/wK+PTz8SfezP0JyPKu8qjVjlzxnnFPrJQZiv +W4kx7R52kYZHAvzYWbCMDzYcNB+kBhmIueVUrlD6qbrNxXzwUOboRqUgsvXX7sfv +bMDZdWYTI9qjvptH5VX/LdSVi6kz7PYIkU1gGXx7hhhXUIPyPtyzMqpnKYXv8rE5 +WrIYi8rrEEt8fDqTYUXl42YslOOG1TuRfCvgmdvONODmYq4P01f5rax+beQOa54n +r+K9RQcy02qhF7JUq7xs2YZ2cinnxzS+xzIklnugJCQTrjDcz19/zykY4Z2ZSzPc +KQ0qRvWeunDQwxc1MLQ2MeTu4GyxYm9Dq4yM+6tK4ZtYfZ1pMvkWfnUbjUeh1fTp +T3EuTos8W3WfXYI8uH1LjCuxtYF8onxhFyiQlxVtcIbNJwzjmgvO4RjqX2VjAVRd +Zm7SPjIW+sWOab+FF35lQTVGS3U/EUoTCTsHpgSY1JMn +=uDN6 +-----END PGP PUBLIC KEY BLOCK----- + +pub 3D002DBC5EA9615F +uid Drew Hamilton + +sub 91FCCDE555C64A9F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=f38L +-----END PGP PUBLIC KEY BLOCK----- + +pub 3D12CA2AC19F3181 +uid Tatu Saloranta (cowtowncoder) + +sub 575D6C921D84AC76 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +96Um/Inf +=ec6I +-----END PGP PUBLIC KEY BLOCK----- + +pub 3DA731F041734930 +uid John Ericksen + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFBvpU0BCADRKwBLH8FaRA8vg7m/Mqv3seEz+AH6zfpiDI/4dlh9zD/bdMYU +hhG79AUNm5aVsAz6mkrTFaEkz46Tuc87pE91A5cxupb2yPjDzYd84xXrkKEgybup +9ShnkMa/szRKnR+mM/9LA4HZqnuTk7M8BYKWbZrewW8MYZJZINn4tA6KaAkjvZhl +dfTBBq8dS1g4Kp1sRXpfbtv0QhmWc8KxT9bSwv9ZiFV3LmrVY+3LYqjWQjpa/Gsd +NBBttaaZq7X0y7W5chfrFndpgZ95lXC+8Kcc/XpJmYmNEp90wTrvRMkfXKR2JBAp +oHmUXv5+wgXQGlaL7ZesrcAUr68TwcLU5Ob3ABEBAAG0JEpvaG4gRXJpY2tzZW4g +PGpvaG5jYXJsODFAZ21haWwuY29tPg== +=j4aR +-----END PGP PUBLIC KEY BLOCK----- + +pub 3F00DB67AE236E2E +uid Pete Bentley + +sub 6B7EF7B18190F4A9 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +8NDEEKe1MmtJ2sOJrXaEDaeIBbaVgX76 +=+Uvr +-----END PGP PUBLIC KEY BLOCK----- + +pub 3FAAD2CD5ECBB314 +uid Rob Tompkins + +sub 3260CB2DEF74135B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +o6QtwZ1S +=4qFv +-----END PGP PUBLIC KEY BLOCK----- + +pub 40A3C4432BD7308C +uid Michael Schierl (Maven Project Release Key) + +sub C0B9C2CC3DD97C16 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE5zrtcBCADFfU0ugIGUCM44fqPJKrsB3TaDu5EpauvFfYqUfyookzMHSKtB +4YqBSKzBEiZ1rFB/KCn7XJTh5epoCau4DsG4U0XZjsx+esDR4ZtL42LEzeMTuluV +9eybw5EvW9GnvUrSOq4U1xFdQgCmBcRBPpLrP4hWUXgNlRTEpgHemnDmZIV7Jcyx +KZYQFoddPbUbIOutoMecl5flaa5uHe2kHp+R+PJ8DlQIKa7qsxsAwZhuamfApX8M +NYQmY/M473IVH3ByD8bQ7uc9HM/0q3f32KeEVHEYTKLs4/wTY5mZTDxndQeMpjF+ +8+LHbr4n0zDJERJnrOzgOoCW/bFa8YQv4ErhABEBAAG0TE1pY2hhZWwgU2NoaWVy +bCAoTWF2ZW4gUHJvamVjdCBSZWxlYXNlIEtleSkgPHNjaGllcmxtQHVzZXJzLnNv +dXJjZWZvcmdlLm5ldD65AQ0ETnOu1wEIAKOf1gsFUdUylLyP6hzc4RAgoFr2eHDo +a3w49fdcBflq84QSIjGkrcLggLvAP4eqJnaUhJh+8a4CBRg4FrW1bs/nhdC9rbzF +SXkbr3oG0RafTcTtGuP2JzoVXifY5OfNnia2fHIptex2hJofoh83yCiU36MaFgQN +lorK7/c+K733aEk4KBzMfAU3JiKrKsSTE92Vd1yh4pQ9gMANAPzPqMfcg9XiIKos +8d1XDQtndQSWccBNs5EZWMct++XPYOdZI4bwsmj+ayuJPbJOYG1T0HEA9x0vBwSC +fGEoyq6+ZqlCrxcAiuEqpYMUlPz3ZONUfC/C29Zb/0Q4AuZQPug2fvkAEQEAAYkB +HwQYAQIACQUCTnOu1wIbDAAKCRBAo8RDK9cwjNsdCAC12L1h5yiApA8v1nJ2YEGt +CHciX9B4J9iGnOHeU6XTquPmCgzANd65yLaeA90E//CikAtlHUgiz7+fGyrGbXUD +zS9yu97YP6L8Gfha8UylBr0Hm1bIFuNjG2C+SUU9DPfdeqd+Bh0ygYwnB6DCufrb +B9R0ElvqUDttyiJq9m1k9gCSELKMLjV/1F1F1XA+2k8PjcZKDgAXrLUpu/boCcr+ +8ozuTBTyxcL21w1nW7VignaRRt9DfvHlsli+5W/+LpSuq4XcaAgcX8rikT9YEsJo +UOXnXPP7mF30ChAY5F7mJ9BTe9RZZmnjA2cUdtHLvchd4fiFbu191pbeIk/BSf10 +=hyVS +-----END PGP PUBLIC KEY BLOCK----- + +pub 43203BE58F49479D +uid Viktor Klang + +sub 1364C5E2DF3E99C5 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=JZti +-----END PGP PUBLIC KEY BLOCK----- + +pub 436902AF59EDF60E +uid Sebastian Sampaoli + +sub D94994D14B55169B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEY4fp+xYJKwYBBAHaRw8BAQdArb04PVwQKvEhtUEmEu7/aASZivOWgEkZBqX0 +Tovwvq+0J1NlYmFzdGlhbiBTYW1wYW9saSA8c3NhbXBhb2xpQGVxdW8uZGV2Prg4 +BGOH6fsSCisGAQQBl1UBBQEBB0CSPWzZfBjKWyPW+D6RDRLFz5xlO9/30yGD/VhA +EPXybAMBCAeIfQQYFgoAJhYhBB0sfvitoPeUtYx8Y0NpAq9Z7fYOBQJjh+n7AhsM +BQkDwmcAAAoJEENpAq9Z7fYOTMMBAKfZb2ahnfGNBt8Hrbu1j99580a2IaFQddAk +xXZy2unHAPYyfxDLPkbTR7Mm4k8Cva8PCcXotDow4bDLm9rhwVkJ +=Hgs4 +-----END PGP PUBLIC KEY BLOCK----- + +pub 44CE7BF2825EA2CD +uid ICU Project @IBM (International Components for Unicode Development at IBM) + +sub E01173141D06B1BF +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBEzQQMUBCACbwbw7tuTWgwPsDAdQTWGO355jP75oBLHwGgEwV+OCKtxkNXNw +wrJqXst83vmD1dEJyHflQww+d+Olj90IefQGfR+K7O005C2nky7eNGIomxaP52Y/ +90+tmw8qtsI4nsPWPuVj4WdFvlFgUwIZ0SmX4CauVzg0Ris8f0taxg7PH9zEvICs +G/WAXdB9em08WDD6ruhMAvDF4W8Yy7mpGmdWiFD+B9OC006tv+GzYAvUHRFeCnnT +SoKRiBeLejW+t4kpdMnEfC9ILAYBEEjNYvBIyPdPKBwNfy0yjRebsUf0eNmjGTpk +VPlfofjVaUaOZytUOQvntYpocMX+377DGQIdABEBAAG0X0lDVSBQcm9qZWN0IEBJ +Qk0gKEludGVybmF0aW9uYWwgQ29tcG9uZW50cyBmb3IgVW5pY29kZSBEZXZlbG9w +bWVudCBhdCBJQk0pIDxpY3VpbnRsQHVzLmlibS5jb20+uQENBEzQQMUBCADad+Zs +ICC1lkCN+hdkslhUxRfM7qY86pXBeFcdTI7Nq0GBK79arNPUyXzvrsT0QL2tHyop +PTxZmdLyzO1AL3mYGpJ8400L3jmgCbk+To7naMqbRvNYpRzuuvnvUZ7sleoEiDZ7 +esJ6uJ/CkT6BJiP58TIb5gQj8PmJsRt2XFOWZKOBrYywOCdNZ2oO42Pix9PKhXz3 +CrAA5sexLQkbgXF4ENtNpBd6OwY1C3C1d0fL54t46eZ5Yx9LyRuVEkmNmUdWxYi7 +UwjDvHJvoyLmstuYlopeQ2nowEiF82807UmIPUpMXrdvQFo5a7dfsymbDEiz2yie +qoXVuVzVOEKPcnMdABEBAAGJAR8EGAECAAkFAkzQQMUCGwwACgkQRM578oJeos2c +IAf+IDqhh3NAnNeuT2YIMG39Z8iWfmn9EIilmrIKFM6OGuzkiCEpgWDxp1v1T+BD +LGjRIZ6lit07vnLonVus+zFh0bKi3L2FCnomRNiIZzD/OWndkteFDhxPHG5C8HyB +nvqyLvs2Kc8nOW3EWKni73eQ5NR/3ltP4bGnVvaqUlkERsVjxLDKmgIYq3rdJ7ZR +h9ooLMGe0mDWCzbSu/MKaRaqhR3xb51btVzZokww7pNiKJDyBkY3ljufXkkf5KLY +NM6MeEvgiW0XUHT6tqHwzaigiBLYOsuMjPnXO3O59U8ZOd3zATM13xpyxn20xS6N +YaWHQubHqQGQNV1i0YGd/ufrww== +=/t0B +-----END PGP PUBLIC KEY BLOCK----- + +pub 4604091C01C3086A +uid Dave Brosius + +sub 8C8D68AF389810C4 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGQSS3oBDACzF8yjumbMujh5LRH4c+AaUkaVfMv/tiSCWXANtlJrU+XRPeKw +Sm/jmT3g/W1cTbbTOt9I7329jrqbISHYSzmG4Q5f+KlHl2Mine6mUl6ipuIKmwhk +KfePwNRLa9KUwxSPzLa0N8mB3PZjQ+FQ22IQk+BGwWsFFClSrP2nuXVi0VPctBWs +jh5WUFu2dK2P0ZWJdb53ZJn53EVssvuQfanUqrxkEgvFc9yyIy4HKaqzHd8Etze5 +2NSt8LNUpdkPU4Agla4rhvhXIYQ8lL1Mhg3DAkmC7XGLuYxZq29esu3KYz+NfNrz +4z1E5jxm17BQ3x8aP06TSpeTFC6GiBRzMz9l4ycUohvb+5fLZJVBYIHY7xBhfU/P +2q6ipQPMo+t+bCIzMkQIeinsXq0LV8R7uODKUjy9IVg/ssqhcUovf0M505Pfaj30 +/9SZZ1vtNf3QUTbsHtINTl9bB87txqEp9tQDSFmU/cFynTeCK/VAvfrQyTUmFI7l +FdmRyVM6RF2chVEAEQEAAbQkRGF2ZSBCcm9zaXVzIDxtZWJpZ2ZhdGd1eUBnbWFp +bC5jb20+uQGNBGQSS3oBDADpK2KWHLVpVXJjkMnqr+TSUDW8VnEkdA2feLtyZZ+m +AoLto7SU/WpIpraWTvmBSwJWZITnKaOVQbNeVVcbv+HE3KkLwtFVeV8cjK2sQAMA +EQT7X+CtBegj6jIUYLQEbFoACnrfQUif0Rd1TU4NgqCLQfoKGq1OMUhfePteqVp4 +uv4dB7YUyntdkiCJgMFO6q8z/m35FlDSSaWdQew1d0WxTZxpO60WfC1li9Sx+h2+ +Ve8Euq8VKM4IyMp9co/oUw8SVnwb1PxUd6Xiz1s38W5F7McqSf3Et8ZasFVWLOZN +RFw0U+5pACExRJX2hiNNBYA9n3CHmtxUXWfuDPmFpeRdPno0DOp1s+Po3TwGcfg9 +EEqMEL19xOIq7Vgcv+naiSNNC5rOJc4qINEbf/H6sPK1WMSLKipWZ6BOf8ECMWex +zaWufsCHuZV3Snm8hEfqiEKUharFfdkdgmlkBF+OH6+HTCnaMlwuILfFSQCPkRUd +W9cEWGqft9AkDDoNP4smmGUAEQEAAYkBvAQYAQoAJhYhBLCHoOuEFlY6/mTOukYE +CRwBwwhqBQJkEkt6AhsMBQkDwmcAAAoJEEYECRwBwwhqWZsMAI32IIZp6pc8Bjk0 +UR9OYOSoTz5Ezmh9SlpO3LOLlb7OUUOuXt5ygzDGE5dChAA1V9tOTLyU8+qfXHOd +cJSJX7oYTIpFDYi4vTQFh9Uo5iXwHXFmuhhUMFpKJNaynhmMATs6l8O6Xt5W0OiJ +D4j5XqjDOVf/NNPS3gro4EKz/Qh/FuVs1n4iT38xJBE0CZhwz8X8JDWdT0eAvZBJ +gAGSZh4AE5KzKj0jQwoiACA6qeOCzJ/sfl0fojDJ4PehxJFfRs2hpTOp/AjjF78t +EGqzkcDrDM8gGCstIVeJDhB1yOvA63ExAkkFR14we0JZliFYHROJmo4GR7wKebqM +3UI989rk+O6bgIk+yEfFGLH3Au3tNc8eLNU0qspE2H1enmb6hj6NiTEoE+7OKrzL +vzqZL9WH1qfOB26bczhzqYBdpE3HRRqvOaFo2o7Rm9biGIJ/QqTk9C0xcI1/5BNv +2X76kgI6+2mxiKKvZiERRfYIye0c4h8c+SrfWHSEEXZxaPtTwA== +=tT4f +-----END PGP PUBLIC KEY BLOCK----- + +pub 479D601F3A7B5C1A +uid AJ Alt + +sub 868FF6CCEF26A83C +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF5CDMYBCADC1/aWU6ZbGZEphRbmjUPNfqh3N5goSnDCou97mmQ9Uq8iBuKS +UXJnGSOHudXK56f+Drx5lGZdLAzveZdqaqb1o3yLFO3PJxwj3Ulhab3O3uTG2eR0 +2Xo7GKjRW13kEfphJrfVIaQq/TiyIG8IQ1dbm9Vuzc5NLDIeC4jxYD2S3hUqCLGQ +BiZAEH9un2cPax+hiT+9MGzdfQwdVrSQ9aEA9mtMhEGsk80XtxXJnd+hw2va6l5s +lSErmH5nMtyKh/n9uo/ap1CfPl98n5VUI8dDtUfWVrqRrkyHgdX+MII0t0nABV5X +rJLneg4dVy5Yw/+FVbLWB1Ta21reyytcIYQXABEBAAG0J0FKIEFsdCA8YWphbHRA +dXNlcnMubm9yZXBseS5naXRodWIuY29tPrkBDQReQgzGAQgA1oQHEM6wP40xPfpa +YBBRAWVoEj/CbAV6BooApSqQkV0cocM9wK905az2FmlKn4WTZyNwiA2eHjHlevsI +jKuHJWhSDVhulcKDi0cD6wTcjctcDWm7M3tvaICbieZQXPH7lju5Ct80kBo5ojdJ +oXGD72kVhSuiF8vOrAOiLOXP8+bpCpY4+LZ4qt3XjSnrkZq+h/vcy9crwuzuFlSL +wMRltOvfirrK/CSQZZtqG2PbT30CWFQ61DLo4DkXeNG1fKpnOaDAMaCedt+pNCAQ +1Vdzy1vT9b85LtOH/CmhumjM3S6x/VWwWZVBXi2xVLFCkm3LlWioSvVs6Na0Vvaq +0WDsbwARAQABiQE2BBgBCAAgFiEEA8EjA4wgqunihshXR51gHzp7XBoFAl5CDMYC +GwwACgkQR51gHzp7XBpoYQgAjnYuxyXaFSbCc2EFWDrBA8+OnlbSgJ3etaIOoLQH ++Czv6+wLYc9snZDBm+IvbzEi3tXfi1TKcRI5ii9wDkti0KcVFrR2tpuXDLWYIF92 +cSC7VyBiyT/aZpm0zX8qP8tjRZvy7mewbnoit1R6ea5UifSLvO0bHqNoswfgv90s +rnuUYMY8tv1sSo4j0f0zre/k4QT8sCTeMDsLsviMIvy1Wls1IPRo1SW1euGGGvCo +bxgke8Dw5QgumudwPqehOZVOkIbuddgcur55ZFFeitMOqdRoXkrGod9v6hYY1Jz8 +W/Y2tzZmWsedFIc4ahuAeZG6cH6Ac8prHrQEz6lSbZ0flg== +=td3S +-----END PGP PUBLIC KEY BLOCK----- + +pub 4B1E11D5A4B91E89 +uid Adam Cozzette + +sub 726F4E5C34CFD750 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBF8QwXwBDADKNLAHhjWUqnLYiO+ws3Hy1du6tMvkR3nfsnIDqpCvSjb+3/rI +OHSyq8TbaGLLuHOM4K/KvrKgjhTbXQxvx1WR5IpoylcINzI959yAbaywBj6gVQB3 +JX1xeZqiep8ZOD5V8YfwFOF5pOidVhabwdkC3xw03ZG9N0izgx4gyou1u8ovpa/T +clEvZh3OnmT8FU+NtwdCDBHhQ6CpYqUzslw3Lcr7gNWJrecdqH4aZdVetGPwJXqU ++9KOog9JNtgOb3FRUSOGiaYBnReioqTvI4giLxKHqast0xilHGUKJvkVgiKBJj7m +kEwwhlKsym2RWVMm69cdk9wOfdLP/gHvqWqw9+eh2cQ8d4p9dqMdehkZ/KMbzeyi +hM11wMNTz+QEAIG9xzYe/tGgoIK8Nn8Ts3jSNNQaY40tJJRzheWZrnUXrpFwAh7W +TDUyHOS2QFCoSLZ//n1YTT1qhLeXLTkX9KpwPZWYl/qJOJhp1P4XfQLEAtfiCVhy +HZA8CJDH1uJPuq0AEQEAAbQkQWRhbSBDb3p6ZXR0ZSA8YWNvenpldHRlQGdvb2ds +ZS5jb20+uQGNBF8QwXwBDADkcS7lqcf5rVllBe2TN4ICFhmIw9AimWRN+FQ84DKQ +mXleF2vkE+13g4YMt2EK5Kz7KWYMzX/EjJPaNCz/YjPStxs1YoLK79AxE0qj7zN0 +KLD43SC5SkC+9neTLuCYR/gkZY0DfF4DgMfNC0pA0jI4Han7yiP945u2UojPmsXW +UQBPdIRJ8XtVizGI0SGIV9HWICL3XEAfOqLdvziyaX2o53SkhY4eB/u1vaJxOql3 +uJnOFXvvb27sQRntf/7CLc0XJ4Kfl0kOZSEu5jj5E+BGyIRdZHfZuVK/+ILrOZku +XKHvcP+jLS9nzjo8HV/AYxZYmRFMR2sf5Kz7ADkHqIA2qqSW0y+dUqp9f+f5KLna +RkppZ23DUJuiIO5Ogn8v4XNru1lwwtQDe//TUVO/kUCRBiSxpTcBwFIkMPUP4fuB +H6xFyjCNFR7BQxvtxxKbuW5YcFcOxdV8WkQ4ncoZEVJ4CKjI2d0qGM/F+frmXAfT +rlkixP/ThJbyDL49bO7GkgUAEQEAAYkBvAQYAQoAJhYhBPGlHgUfUn4MjiTVTUse +EdWkuR6JBQJfEMF8AhsMBQkDwmcAAAoJEEseEdWkuR6Jf1IL/jS/by4WkRkfEgAC +FSVi4sLsb45MjXMsQir62TZ7QOTAIVA2FiUio8Y2hHNMNcs8icpzlMGWZb8vtPKw +zFfCqhQuJmHrPHhTwISn3r2FGJ2nvUzu1uqMAHdVENPWQd94vBxL/9ZC2S8I6df3 +46DrGZNs/lL7VeAUfebg/Zd698J2aZs+mERLnqAAazAxXcRSlR4DwWuSv+4/7+Fj +vxPIjU55QHlf3pE2bELntMR5siFvCKL3wMHorzcw1fJsSeJRTt7tIqFd58klu4IZ +3qvFuxbhmOaaMUDIFJeGKJ9UUp3m4Tvuz3wCIAWiqT/OA6E2dK3R3owOYZLMyPIl +TkXNOSowlAbFwVXHh3dI2+Rbf8KgjeZo9gu2PDSe1JnLc9FvGsFaHHDD/y0puIKW +VPqpelEi+SOEo9tvgUEUUDwigvpiT7WzKit5B0Icbg+moRvY6a0FZPSvsZZx8V2J +AGg2/CyrKszDpsR8R04jHL/ZAxrodA4Awc3BTpNxQDPfdRPc8w== +=SIQO +-----END PGP PUBLIC KEY BLOCK----- + +pub 4D176DC503FB4267 +uid Colin White + +sub 5686B45C142551D3 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF1EtnUBCAChtyYd/4eMAxTz5DVmO+8QOrTA1cf9bprQhtXD5pVbw8/IGKN+ +EqXmvt7AGy+4O633g7ec5iyirwCfEP+4YDv8k1LOvY9C5+tOwfK+FxAPRVc1AAB5 +q23x4yMI7aDdvN52/jqpREeBBWPcrnEIET68RApayALenjUP6Kx5K0ge9aU8uaSB +HsVpHSr05L1nKuuqh0LDNXIYDLCpo0LnwPNfLrJ2xT6gNTwHBY5mncDy4m01rdxD +3to/s2Uu/9Xkfz3+BJKRD2kr5txiqoW69H/qsg7u8tTOr9FhB1T21pjFZsdpzbOq +cKd7UyD0RpyQYHmTeUIhKSI0Su7pKT+RTL9BABEBAAG0IUNvbGluIFdoaXRlIDxj +b2xpbkBjb2xpbndoaXRlLm1lPrkBDQRdRLZ1AQgAxOP3klOByUo1Kyl1O6rZqj9e +yts/oXtuDTISfcRkZyg6fmkhT4kpd4xLSP3xHvRwJugybyTedDHzXXCOSjl3EFfU +GaKJuMqSKs5YjQOWk8S9BegAPeiq6PGV6gbHZQ3Xqy+XE+TLy5N96zu7th/YJraE +NpS79sj//mJQE2d49YrxhZwtMj64X0B8/mDmED+D2cPXAoLxNh//LaV26gpOC9yV +YTXrCq7ODPE5LyoljhBmPZxoapcn/39V6UvVSG8Dq6R5QdahDgNnCEjVPtSzGtbC +B4zpSv/LvZkm8gKlaxUcra5clXL7p4NWGx1C1Ap4+H6U2h53lC7FuYl8k9mBNwAR +AQABiQE2BBgBCAAgAhsMFiEE3yXTxIaP7BdxgvrWTRdtxQP7QmcFAl1P4JcACgkQ +TRdtxQP7QmcrkAgAiSZ2VRgHTY8SV4dXXjYBO6WM2Yzamt4bO5Uqflw7gPO44AtJ +h+Pn89iz37RnGL+bAq2MCNsgV4G+KaLJrE0c/rt/szC7UmwEJmy4DcH5B80wzBvo +2kpN1Uyr7+1+IgHogL6HjLxccoBpawpm0AnhNYrd2ml7STBCfNmilztWGrTkerb/ +2qOY7PmgWEad7whM0OEj1OE+M2IYZUga3gxf0B94GtQNwmI4xv5qXGskTFAlM9ko +94NfvOxEkgCGtnicN4Md74od0POEVrZxBpXuJGt60XVGvOr1iLmlpypWo+PkHhZe +Wt45mNAFy6vraTYYCkGXQKqx1kxg+OurYgIFgw== +=zwEJ +-----END PGP PUBLIC KEY BLOCK----- + +pub 4ECE492B63E38ACF +sub 7569B5DA31ECF7BB +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +uHMN2jfImvpKk61/hgAEI20K0qvSRdKP +=AJqH +-----END PGP PUBLIC KEY BLOCK----- + +pub 522A2E62A5D6C31F +uid Filippo De Luca + +sub 4C25AFD6668960F7 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFMoq7QBCAC915NXkdPuuNWb0ayXpJoPt5yXW7cHmsBuXZEKe19v20e47WXE +Psb5ofeFeCCWvM+D11P+4xiQFf0egNwdjGdtZj8l17tW1dX/lWHN7PHLCkJVvZF/ +TjiiQ37UoDraaMRzOfyjWv5p7rgAnpGA7hurKazykZbXdpmvSSgAo9a5FkymW72s +/v2ZE62dk8PrfEMy1BKE5Gl+eu+ZEtGhsSUAjf3oLLyAAqnlSDfrTPo7iDkaF4lf +h4JOKZhH0f13HUNgUrKThgaUGgCoDudgJatJEmof3nmV6pjCpJ4ZIoSoN4zWKvKt +7VDofvfLQDnAsYV/AMtlEu0o3oN2U95EZmRHABEBAAG0JkZpbGlwcG8gRGUgTHVj +YSA8bWVAZmlsaXBwb2RlbHVjYS5jb20+uQENBFMoq7QBCAC4WF/+zwVYgt1CR968 +0LUwU9Dpuji2L8A1QDIGbD/5iVvPgtKP9xaKiYoxJTnxqmthWGmoBZ+1aaxnN8Al ++KaAbN9pgnyMmJ6SieIX7c/8BroFpvbt3AfG0ku4it+/RMbTj+HFuxeOj/+zNNoL +eIIPN+Mk/zAxsDqNNTujJwHBR8u30WxqnxWJ7XCRvr1/p0Gz0/+GV4SI/x+aKA6q +dDMPtL38MQIloeJ53a54yJ5MQUYvhs5uZiuArZ1TIHMgNXP3MJKZOpZhAanpvB92 +osIbqCpTu1ikiDD+nMKD3ZLW8vHeA4SdFpMsrz57SDVXzCgJHt/GBRPx0J5G/D60 +Hj9hABEBAAGJAR8EGAECAAkFAlMoq7QCGwwACgkQUiouYqXWwx8ukAf+NFQLpQne +X5qEpqy1J80JFc7jl0qPINf8YhkiPzHaj57BCvltnkaTeYLyM4YY4a40Q8rmtYiJ +kigEcoCECSTBIULtL3MZJqLG+UtDluhDz8mJ6fVILEkq7Jc6PKEaM1CrDHhOXLq5 +giyC1cTFuF4glAWeKa+1+goszPjyZ2xmkIQ4LumlMNgGGDZkmSKvswJOSoam5DkP +YyggQm1drySKnh1/Cfqokm6y/E7jlbpDM0BBiqzf7pVJoCV8+NxRYZqvZJOH1enC +5/kHPyuXEMA+CGazW7U4I1AhLPoa4t13THA87Sgtt/BiZAYGeznNop7bUpyazxvk +4sVvPBY5vvR9ag== +=LrUd +-----END PGP PUBLIC KEY BLOCK----- + +pub 55C7E5E701832382 +uid Andrey Somov (SnakeYAML) + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mI0EVdDLQQEEAJMtYCaTA56YsP5RzQzPvqVTaR2nZ27qRk36blHB9WmXK+NHpGeH +PHgq59mLPVueo2/M5k/fFrCe36jHePP31gYpFtueeYDfsofHwod0WhsHyC7JfG8d +jEnSczTCmOHRZ3ed9ef6SeWUozYCQAX/tAbpoCthe0lTDYhFhkzVCe/FABEBAAG0 +MUFuZHJleSBTb21vdiAoU25ha2VZQU1MKSA8cHVibGljLnNvbW92QGdtYWlsLmNv +bT4= +=MKAK +-----END PGP PUBLIC KEY BLOCK----- + +pub 56E73BA9A0B592D0 +uid ASF Logging Services RM + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGO9IPgBEADHCB2evKGSjQutTtIjODtaYUoU634Tl4Iv4DYsOR+MyOQBJ3Eo +9okST4azX6r9Fspgmc4LxpFnGm9OSEGn4NZ9ZxiTrmKaGsoXzj2w4JKtgAip4F5h +o9Ai/ridqeA7V9VH31OLSyZ/YJvrcoBcxWdwcpydxchlKQOOYX6wKqGk0BO38870 +sI1d0y1W9pC9AiL+z7qaX3WUslOFDCGxRSxZs57x28P4pkRy0HwUWn38TrS4fnA5 +XfWuxXWr2K8/xtf+Uf2EGVUP+qiv7kDdKYbZj+SeF4ba8nLPcuFmnz6Q1iZUhA9s +sTfruyibtm7htngt1IHFy7DT35GrIbE6D7W7S3ANqg/K+Gc/j9JXKjOwx8AM/WI0 +WQ5w4fSn3C6WhFTMpyq445c97+5ki/SKbFP45oWd7A9JqIMttMqZLzOSzFM4230J +Cd6FQTGJdtcgfruwltQOUWl4IaCOsCAzI0XnxTIdsaCDDQ6+KspvdFw7zav03eVN +TguQ39IkZgke2Am2He5vtxXAayv/qiTKsyTUxIIDe1ZTS5j3MJ+SNWR0faXvvDaV +QgqVjsB9U/CHjcCYbmFhKxk2phGwa0Wc+NHKwyVwjhJkFo+U+XDGJaT2RbhDYD1H +LeoZAT6kdngSzr2WvXWs6xJbWIXJc5FivUorfkhaiaBWam1R4o+8kSuxSwARAQAB +tDRBU0YgTG9nZ2luZyBTZXJ2aWNlcyBSTSA8cHJpdmF0ZUBsb2dnaW5nLmFwYWNo +ZS5vcmc+ +=DAIw +-----END PGP PUBLIC KEY BLOCK----- + +pub 571A5291E827E1C7 +uid Central Repository sync with maven.java.net (Used for signing artifacts that support syncing maven.java.net with the Central Repository) + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBE9iFawRBACJb4OMk3zqMDNvSJKYZ8fGYrPq7yCcf/ykKDkGb2dtPnAZGkSp +3mmNlTsU6s9ARn7BtkhIuM5TdbLs+z+okX62h3F0WW3h+CpfIXyKSgl7uWbhZ5G8 +RSCCRr7A8m6y83npkTVDW6m2oFN2cjFwPLm/vxg1gu5pO+NCwz5iBRHdewCg1idO +Gl6gBAZVNteCRMVnGnX4EhMD/RaYBO2j511h7gR+p+6BBeJTEIA1+tsi+GhTBkS9 +mUMGuD9Z8PyvwL7quGQyXJ/kTe3eB6iyDFn0oemB1w736FQe3vcIX6eePOEiDZs5 +1Uepv7bXI4wn1i3Z3kzynXNKcjCd5ZxAmML5VlQ0zWeE0W18reCjt1P5q5xxBFjw +0L8WA/9aPi4d5VPakzuDvxfKK29BogScTLn2C3fpEnqWsTfpoWSkNXkRsoB4jUU4 +oIqRFMTxwsjUmjVUPOG+YqoeAaVpj+RBpp+V+CqgfNWpnH4caxzODE9f+6RYRCGm +LSq/6OmgZg6t38M5XWVpvk7Ixygs6Vrd99VZyIQPJwSBM/pvA7SfQ2VudHJhbCBS +ZXBvc2l0b3J5IHN5bmMgd2l0aCBtYXZlbi5qYXZhLm5ldCAoVXNlZCBmb3Igc2ln +bmluZyBhcnRpZmFjdHMgdGhhdCBzdXBwb3J0IHN5bmNpbmcgbWF2ZW4uamF2YS5u +ZXQgd2l0aCB0aGUgQ2VudHJhbCBSZXBvc2l0b3J5KSA8Y2VudHJhbEBzb25hdHlw +ZS5jb20+ +=2jV3 +-----END PGP PUBLIC KEY BLOCK----- + +pub 5796E91EE6619C69 +uid Eclipse EE4J Project + +sub 153E7A3C2B4E5118 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +gfA= +=Sm83 +-----END PGP PUBLIC KEY BLOCK----- + +pub 586654072EAD6677 +uid Joel Orlina (Sonatype, Inc.) + +sub 2E74CACB6918A897 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBE1VSkkRBACkCgvt26sMi+0X+EOJDMqdK0Sziy06k47LJf1jOg4tTZ2T9QtP +OZ8fD+va/O5+q8Kna993jzcO5n0Nv+R/K3+MvUqSmdITshCIjBt3cC0n6FWndGyl +jY7rOmzdNnvSkMGE3V2fQ18stkJRleqk3EuWwv+EwpxcYeVfCO+UCJGz5wCgtqnZ +JYdRHcDkDYaIZ4eizpbV4d0D/3cgBdpcbSrwIGtft+lDxacaJrWpT5Jh5P0gLlYu ++6PFz8ZIC4+/aOSi4S4mgZxk8dBL8ZBqLqcW9rc//CYKNtPq33sdf9vxcusMIXvS +PBODjDpoOsTJwy51fgCEL14qnp0v14y9p7ejjN5+GipiNY/JHo9S9kTdVhMYqt6x +6a6MA/40vMejIbZ4q3Ia63jbHPi348fLDq3Gp8Wos7Sh2HnLC+pRdC46qX/5wL4t +Vzj78yW9FdH5yeeE6nQLOBWh7PnSfMt2wYHoarEnkkkycP7WLpRME7qsBYqkNUNa +2EQZSy8DnGiayYDij1YPNUHI9kpK6H/e3puhmgNkzrZj26T85LQzSm9lbCBPcmxp +bmEgKFNvbmF0eXBlLCBJbmMuKSA8am9ybGluYUBzb25hdHlwZS5jb20+uQINBE1V +SlkQCADrG964NxqHiAULSXYEVH1CwlTRILkiTBEkL/cZyoBBnkF/SLVX3TC67y23 ++MksiCdUoQUfJPNF5PDOgtlwPnNeAFVK7T1B5rHjud02NJ9lY2Q+ZrO0zmP3Kvg4 +XkwS85cTbCvOatI2VzitvkpYr/WkQSJf/N3NYkuZATO+n8y1marIrqkLC1LxL6Ap +lCNlks4Sd7OLN5whx7avrEJHGi2qdxTB9SD6MxRSVkpSUrCHhOtgMUeF63xrNcum +MQPQBIbv+kFgNtE+eZaFfU0+IHgy8sMROvHrtqzPOuwZdnH1OvrVuEVtErINjmJr +//nTXtWBK0YCpuSQI1kcadCc3OO7AAMFCADoDnwynvulS0K1WF8FGfOIqQRoKfyc +Kmoz88WvGKHTx9AnfG38Nt+kHHfUeTLsozW4uDNkSd6S5TRmrCArdqpaDzSagpok +3FRl13mit4Whuw7um16O3miZfqTF0aM9yU63UzyxzoT4DexHlrZAL/0G2zjabJzX +Morr7p3dSl9SWqBo+9dsZOyKwzuO3gO1XPqZctPKMCJZ47Tt1xtKOBm5GcexIImh +vNOY20VM/UOBR5yBNw4rZqOAay7MuqRtpCcr9GJEjOqQUhaXPyvOVg1eHxMD3QEt +4ZdK5U4RbsYEhzx9WezqjXQ52wCqfMRCpwJm/94cdeO31PlaBXd6nLnXiEkEGBEC +AAkFAk1VSlkCGwwACgkQWGZUBy6tZndAQwCgs/qSu+5vFRvBeGVsg7YSIxOHf8wA +oLIHbQ4IMkRivPgSpuxw53Hofe7A +=9YZ+ +-----END PGP PUBLIC KEY BLOCK----- + +pub 597852B0B140F0BC +sub E0A468B5546CD437 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFFDPRgBCAC7Z/0fkdEkWVCD5BCDrIj/YOZBFOLHAZWLiVhyX0iDO1fhJcj7 +8PKGiv+tnLz/FC2gk6ZJCbwbAlxb5hXLvPWe1SAn6KjOvqNKx+qZ9LNMo4dCqlBp ++tAB0yUQVAorl33vHJWV93WaiKwsFpl6Gzr4i5Fmo4G5isXaDMO6NXbNbx45tmfU +V1RPI3EcIl/h5jNyRlJqDkoNz3TmgVpt9UO9hH2cGsejdj0c+56/4+/OE+IH15pt +w+oRt1dmGFxLlSA6k60rIzc27stABX0ZXi0WoPKGW8psRnZKNVf8/9d8IaGQivWu +sytoVGsaUmIjlJnZYEIJtk7xOMrWqmJdqsHvABEBAAG5AQ0EUUM9GAEIANjKOJC8 +CevFnuPewZfDQTFJtSbRxw7BmfkQ2xRM6NW8eapIRATJZB5rolc3X7wQfctQJPDD +yzhtkIS2ogMzSpuauMAPm+qYWGpT8pyq76d3h4do5um9zwGIQdkK0YyRfHVdyCzy +PxE8FUYmBh77ooCLIA8ihl0HFe8vjRf/BYZF9C9bcIh+r/1LNOwdAKkDrSIR6A6M +iax8vJhvGiyQz9biquMggySL6pDd48ktz5QhrNGce0d25b698QSqYeVWFx66niro +R1ek0/lhnzDsrK0yVcKSUy01qnM9D43g6GmJ2vYgq4+53h6P12vY0ck2KnzFFpOl +4M2S9k8cyJ5dfWcAEQEAAYkBHgQYAQIACQUCUUM9GAIbDAAKCRBZeFKwsUDwvFjZ +B/MEjtMILcxKKDEH7G7rDZxvnIzZwPbYQm4q5bzgX/zgIcjLNBbY2+pli7wSSfpl +rpXNfkNRSE2koyxBOOzRjVNyRcA/mKfHtuo+fkUEqdp4lXM6FfSBaav9qGBRbU0p +3XSOYigJ+EHomYRuaIXPHi7AOAtG20bNa3IejFQT8dcLj1ZS1/ZPFrPcWay/bDbA +QpHobv/5My04QmbgUqmvV2FWxodjTc7j1KXgINux5E4NruiwaCHVoHR0F/d79wHs +Xv1s3ITZmLZFbsMT1CusuZOGRZBZR/v41c1d2Ju3KH/G5+9HslfCsrWaxK/uN4Eq +c7/hvC5COsGvtYPqHtHFSXU= +=XfhY +-----END PGP PUBLIC KEY BLOCK----- + +pub 59A252FB1199D873 +uid Tagir Valeev + +sub 92BD2D0B5B21ABA2 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFUBG7QBCADRWXf0Fw05qRhM4cRnGKlOW1ecue1DCxHAtFwoqmAXyTCO+tI0 +MEW5SyXUkX6FsWLl6A2y+KgOs669ogzfQ0rnZMEt4HisRp8wpgk3GWR1/9aKYz/c +ymy2N3BP9cz2fJ9+3PpBccUPL+ydFKpcnEnIwiQK+p9JjEWzJBlrdUc/UEJ0R+n/ +5r/+0+BHiTEMvjAF6/SwyntpTWpu7iEzLv/pfdCuhFKa4yn+9Ciwe3wGtSiue+dh +tqKcd4YxED3oAswObBca3CC2HWWsUEH6EmfT1jUdfy1cq4X5x7AZ26oFYfG+odqW +W5dcB+13VkJtJRzQTO/2HKtITJYC65a1jKt3ABEBAAG0GlRhZ2lyIFZhbGVldiA8 +bGFueUBuZ3MucnU+uQENBFUBG7QBCADbCC7lPXB/xCBC/jqcCGnK/8t/+ixvqJPE +igxyJRenEqbrErFjOi/kRnGYLwg0dEtBBIneOMsvMBTL6GEpbFxyzeEqh/66SyHO +Ag/A3Qi1q2imkWa4baszVkrGMRIKqO59cTuvnLFNe1SQK56ZBjx6AO6KGZWhq3NM +v65ZE1x/viyqofJ4jvQ2qeOqSxa3YL7sim6tQen2gH9iTEcr6stvn7sH1Rk3OwxF +FBbcBoOxZ4gxdM5ft6xRtbnfZB/FFs/hsAsBU+qoVYJYDprSYMNQkmDXg7ELwweG +EyTZzJ3jEnTOgpBHEYS6dvpc/dPsEdCv2vUARNTT7mwGkQdrkEeFABEBAAGJAR8E +GAECAAkFAlUBG7QCGwwACgkQWaJS+xGZ2HNriQgAxxwfwZnOPGHtcZek+p2zRIjA +nZqSG2viTRZxFnLnquMZNMaF11EqQZ4y2lj0K1WSh13TMZpkdwY4bRb7C4Hmo8qS +1JFQ5SjJHRkLbFly9Gm6+HDaDA4l1EcZW14MWfPoSLP6yklirbq6wg9leDFy7EFe +MQK4dXs5CRRAwN7URs444M4OTMJq5i+x+T3Her1dSnutAZrxWL740cE+FMNTg9F5 +brjzmmok4m4TxAnOcy8Qc/fnkUrEW8XHDRMz2CUvF5ffoSMO2OzndfOHDqHscXaC +PyudpB+wOcnxI9pFwmZubWMpcir4BqXM1nWbqFd7tcYPre/0JYIUzKCQANB+Rg== +=NhW5 +-----END PGP PUBLIC KEY BLOCK----- + +pub 59E05CE618187ED4 +uid Taro L. Saito (For GitHub Actions) + +sub 8857595B73BFD468 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEYuRVGhYJKwYBBAHaRw8BAQdA2Dp4m1Yhtb1g94pQzzL24FuP6b9KXF8lP9Dh +hZnynhe0M1Rhcm8gTC4gU2FpdG8gKEZvciBHaXRIdWIgQWN0aW9ucykgPGxlb0B4 +ZXJpYWwub3JnPrg4BGLkVRoSCisGAQQBl1UBBQEBB0Atu9kejBi+6wfOT0a9z/LY +EEdNXM/VX6xt1onKToPPdQMBCAeIeAQYFgoAIBYhBMHLp17JvQuvgGGTVFngXOYY +GH7UBQJi5FUaAhsMAAoJEFngXOYYGH7UlMABAKyRCazhVyUFg5FOpAnmckBY38Ca +MGPPLXVyY8Kr6dYFAP9wYLu7nsDZCOXkAgS+et4Pk1WZCggoYUkxsX1o0KZXBQ== +=7Gio +-----END PGP PUBLIC KEY BLOCK----- + +pub 5B05CCDE140C2876 +sub 9D29AE4A6B50E01F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQMuBEwVZOURCADNnKQzSjFuI9/IGj3WTJcPU2B/H8NbZaTsz5WE91WumgZulK2q +YeD4u6zdOyFK7DEScgxk7dicox9cNEgYKQnQXctDhfqER9bnvA2iJ+AFxjRAWyvs +en3ClYLXT5UVx0H1ZfDVKCvmaZVirZInfkqbi3OiPQoWrUfu02c3DiHQJ+Y34kdB +egH2sIShNH8WLfEZ3YDQ4XaWHVuN1C7VwCBM8R3OeTTfyDrTsuyqJ0SeZXRR/6df +2pEckjF9DNSXyjzFg24MrZhuhgbnj0oR1zmRh1EF+KlBfF4DF4acfxWqqcJVJx/3 +FTtOkLe3Xjj+inyJgxOW52gD4DsJpyf1tIbjAQDZvOdlRRCqZB4FnzzIb/1GmkGD +JpDLC4MQmqgxkm0n8wgAmmHLpqDTdmuyJXvdX9RdGycpW64sljd1mpzTWJ8UKDhj +uFQVHSSEdLoHoVj8ItnBV2kXd2uoQ/tWzbxFBST7wE/tX0e9G5XWaPKogvOKeDus +u9XTIds2krYp80UTYWFZ88oNwGikdIrEYURSYDsYt15miROtKHWbSOHeLVbZqgVx +dtWPqQVfH4gJGEH97/OSmozqDVog1aZDKBLGZQosng5h4j2RAQpjkaIdxKl8m7CQ +x0Yi9tA8yD1QhRGggANQIb4n00G/vm7RMU/7NBvvjAQ/nAFjbsyO5oX1rBY1M6Xo +NFqIBrHSBzV9MmhS3nXU+ZjAktCRhyJ7TsoHM0OYEAf8CduM5Zzp5w02iVYkFQBB +wAoKHMpycW5LhMMMS4w7gmOfP7y04rtg6+zVe41y6bOqn/SxHCcCgnE/nhdexlzH +ElYE1H7+HpphoI5vEwS6uElF67CoO5r74Zrb6nshGEj2AoOqjbrsdQm0noBBNYAu +f9RsjU0sQQFzLW8+2xahqK3oZkLWOkSxzLtVwJbm7EGaGIYxEBjg87OnGQkAi9vv +tVPwdO3VWyvgKLuPHudLDhTpeH3AMbzKgnru1Pnh/ZpiRhPzsbuFtFPEX8PMuCyE +n4OLzUALl98kXuPjG5ww+24UsNgKMbKbu8qq/zRu7IHlpZvd730RoCWU2/i18tnY +zLkCDQRMFWTlEAgA+MQFGIhyA4Ww9g7J8ZiEltwSzRblrjM1q9anexsBIGsWH37A +92rlVK1RzMVfhj5yl+BzIBGO+zHbgycX7iB5/Fwsm+6R/2Uich6NDm1Qai9rc/jg +3MS0phOAQzgxlGKOTS2GzdbDJCBQMijDObNe+Cs5DNB/E29/nzzCTQvtRzSeplZN +r+8Q8lWz6efXmm5EeeZxN4x1YXjjzMJCHbc3yGxOjTgYQOs962yUYsg9UDRJm1OH +9NKZe1m3dTRIMUcZvL12dq/kyiHHR9V/6CkdiNw1AFMi3tvEdvX4D1k1/Qr/2ORZ +E4lRzgug4sKkpgaclLnkJZ9EMczmUFTGbbkx3wADBQf/Y+2nZCJSuHiDv/+SdhQh +OBapZ2hYPDvg29mpPqin/LwH7eFTNv/oos1wzuzGtTHHGEP5mUQLOxjwdAXsWMMj +scSbCs66ytTN7X4O8qh+1yN7vrM6ZBL12Ix7Ku40cgkWyvTVLBXKaEGm4ElhAmSL +Fpu+/fJw0riR6rIuwHcGB4R1IJtMWcj+b1odgw9QmJ8AGpHh2WVdXspoCGnTUN4m +DEswZjplkKXCgLypU13SrHVOqhjd4caK5GNZUfWtCKtwNcJMnvgp2truMvh9BBn6 +widfK48hEknQtXzGjui+bZz2/AD7/OT/T1CqDspB8IQlBCMBn8J4U1grSrZ1wTJf +HIhnBBgRCAAPBQJMFWTlAhsMBQkLRzUAAAoJEFsFzN4UDCh23wsBANDSDn2KWz7H +b5geDwUTX4T8Uqn21eFbp54tFTfopCd/AP4nTdX1iahsClr9q6G+CWQBuQWHVmq3 +FlPU/jTn6vXQwA== +=dKtU +-----END PGP PUBLIC KEY BLOCK----- + +pub 5C504E1210E49773 +uid Dave Brosius + +sub 9344C7E75E48ADA3 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZvif2hYJKwYBBAHaRw8BAQdADgAH6bMuk4zOzywMl2kUy2IYqiQF+R/4qYUL +KQ9Zq2i0J0RhdmUgQnJvc2l1cyA8ZGJyb3NpdXNAbWViaWdmYXRndXkuY29tPrg4 +BGb4n9oSCisGAQQBl1UBBQEBB0DPBdgnYQNLxY7c/uAopyjsC2v2xYyAPL1hxcUt +TqubbwMBCAeIfgQYFgoAJhYhBHekV0DCOIDH+BudTVxQThIQ5JdzBQJm+J/aAhsM +BQkFo5qAAAoJEFxQThIQ5JdzM8wA/2kla7yBkpcffOAvcBjP9vxJuAPlHLVlMY+J +mAQzOOYVAP92fNohJLnu7JhsE+f9U9IlWpf6LunQtPiknm9q+rZVCA== +=Op+a +-----END PGP PUBLIC KEY BLOCK----- + +pub 5E1F79A7C298661E +uid David P. Baker + +sub A7CC6488427379A4 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFgRFtYBCADud9fmvTI8Dbs+9GcZUIVzxkL84QYHSDxI9fF+sxfAviq1U+YJ +a+ZLIW7HsXx8vpn3hqIqAbDxHjrb6MEJ3OWD5Ks7O9Lq7HOhtqAT/mpV3fZmf6pF +zdEw7c4UrfbtKyBY2kSBpKzTfu6HD3q4OBDm59Ezs2XFhKrXtlNC0fQ30ysBpIvm +vZH/opwlBgyELKnAYJ6eCmdW1iiju7DPKDBOrGi6zgvslToLpnZeSg6hzSyjM15n +Gx6Dgby0GNR4VEVze/UdOpsFVTSfP9qXgdt5ZOWQqW2Jg5V/ezvk+3Ok+ecfHWRz +q8tHkagnqn0SfP6mLqUNvmvAH7xp8crH8L/TABEBAAG0H0RhdmlkIFAuIEJha2Vy +IDxkcGJAZ29vZ2xlLmNvbT65AQ0EWBEW1gEIAMuetYIGcqEC7KdfWn6EKmO7Zucf +OEirvo+WXclo48WX0Eo0gsTghKPGTS2kOzglwn/wYCbBVKzYaOngZljIcrR47hJi +Y/u7OH7EjiCiB0sh5WuEqOaCPPFo8lCA1+SBPAF+c1d7SfIEABL/WCc6e1rkKhe7 +wkBSclspL8YQUG3cr5G/cSCGOV69TsCqq7rtezjkSsfE5dxmcs39Ouur7hs25DKe +hufUA5bV2i51v49WIuTE8x53VfInYsJyeRs7f4sx3hmkwN+EL2mo1YFymGwEkp8i +B0Jtrpsevl4AFOajl6X4IrdLn6+XSok/1mzIm+t1ZHokQ3mUWe5FC9c1Y1MAEQEA +AYkBHwQYAQIACQUCWBEW1gIbDAAKCRBeH3mnwphmHv59CACEnAU1vbN4qxquAzNu +aalyV6Hyx9olUQqPHopRGBA2ulPs0l+gtAXz5USotNsh3Ai5j39Y4J+qxN3HuDts +cxEReogawzOo/B+1IKuGuuTzvL6fU6ZFUnEosxChAKwJo9eS5xlyenyumTcXx5yB +/5X5nqTes6tcZlDcEefh7K5IaazwE5caITBX0ze8g7WQzRxyN+vuhY30U7P8TTKx +AsavdSBVIb+Hp0e2W2S5T3ogXaGkIi5qllr9uhfX+E9zLxJJKfJot33ix647mPwp +Xxo7K6teo2rkwOTQij1sEe2sbMZiKZkn4rYSgLpZiVLPiDYuP3RTuHnFenYROA/Y +cDvA +=Sagc +-----END PGP PUBLIC KEY BLOCK----- + +pub 5ED22F661BBF0ACC +uid Igor Sereda + +sub 31ADCD8BFCB760B4 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBExyNhsRBAC/W5cMapoP7NUn8S22iWG5bPw0bconApJHP4kQdT17gT2JgNJz +BmuGWV59ZOGQkc6woeFKc1s6twlsgIL51jMeVOtgLJRGTS4So2hthNqDcgO4j8Lm +yXpqbTkbD7/ZlRzL2hhedrMz4NQOZCvsZpQ1RaCDrr2hxDq/HhD2omGdlwCg/9Mt +JNc7897LgfCMmtPOvAFt+rsD/0K87nvW37nlRqHdEtzvwUlyLJmYxdW9hDr8tm4Q +Y/8rDvNFlhKV/yXmxQuhtgQ1qpBo75dwD86aJmzIMIWM0iei9Ecfu2DsWiWvArq1 +heDjMYSeQl6k37cmD59afo6e/jQmg2/ALC6mRf3912SfmqV5spw0k+NYdFxAnbot +9jOfA/41shIdZloZ0aDcJDTNe22wFFh2sW8RwWtJJO8rmOCgh3MmkPn7LHPI9idJ +bSdD1dRcR7UTyeigEeDTu0PAKfKZutc91lfcIGSZdk39SEEhUkL2JdPKVRBotiZZ +Jsi+NxDdsprF/yQtr00XSGJYzh2TW/Srnb5nZQm2Iyokod3M1rQeSWdvciBTZXJl +ZGEgPHNlcmVkYUBnbWFpbC5jb20+uQINBExyNhsQCACTTDNmrbllfvcMiXHDfS7x +CbLQczZxWZF7jk+PznsWVJCw7zZg084zFKQAIKYcABPekAK4wJ/lnQc7mItsevF6 +AU8ZYuQ0TcInnKBrXMiIWDM1z2o2pa09AhIkhCMAGK3hKx4xjyL7e3oDz02NZz8A +4rUGWgd2MCa29Pnihi2oVgIyU0y0ItpHqKhJQffyotWNe0W+ZNjg6qiQ1rPolfHN +PKbKjyAl9/0lGlybZLZeSyvR0s1iblDAkQ3spfgTyIfxHPxNbF8CGt/TOfjkPtuA +c6TckU8RfMoZXWX3pNvmH/LL3C43gl4/JcMZ+KC5v7WCtXkMEpDeyuQTDLqLABjD +AAMFB/4tehSPbgS+Fx744n1kJo8SmmZXrEn4tpT/0PWHAyRDoJdzQgfeh40YBts4 +eFczYbKZF+DyebNsgdW55M/whJ47sNz6AyvuXuXmQHVbsiy0yVLXEjdV2D0Pwdo8 +lrmAHeoQasg90M/UpFHAl+XD4OZBcHmM6Pi9bXEQKpfRsZj1c0UVpIyGSvldFVPf +GBDx3TC4IH2i25RkL2nGbAWIfbD8W8rMxMoxHArghE6fkU+FI7ZUEcGeW8X+ZJsQ +PbaOMHsOeBQLisXR8TUysDoMMkgAquwOWTRUjvH+0g5huQQ7LKkOAF/gkuvGCsjq +OfeIJF2aLnWFLt21dfdkiSYNUms5iEkEGBECAAkFAkxyNhsCGwwACgkQXtIvZhu/ +CsymUgCg/kZJOCScWbc41n9bn2Ir6X5claEAn0Gu7stpIpkazuPCpeoY2tnRnPE3 +=ZFxP +-----END PGP PUBLIC KEY BLOCK----- + +pub 5F69AD087600B22C +uid Eric Bruneton + +sub 0440006D577EAE4B +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE7JURcBCADO+9Dc4/JnB+wX+fq+Fr2zUGSPOT6/qjE5kXL4FEbJKsqDSAKG +VnbtRrsIUdmNIFQmz71bBDFhRBbrSrkz927k8eUPhYtxE2NmmWSuKgrjF4qviPQv +m/7SqGx378m/qw4EvpgGUB8EYif98LYdWp5vsU/zx0Ps9auqvetAzJaL9489oE0F +q8FVhve6BMfUUV7zOTCmJnf438YO68upjU0PVBdfFE6Qx4cgCeWbQGy2cooW5azN +iIenhuYU1qikmxMHq2xZzN4uSTWLGDpimPyz+Y1aTSYJ/bgn9gPStbI9sojWo9SS +5gvNK3XqJzMwxwFow86UcIE0vPD2T6ZlBAXRABEBAAG0IUVyaWMgQnJ1bmV0b24g +PGVicnVuZXRvbkBmcmVlLmZyPrkBDQROyVEXAQgA2uNV77VI+ARj1d97b5cY3/er +0Mcc8/Q9ctMY+5YpSYDOQF100QBdOQ8q3IJsfhZeF/iMFlHIUikuSgatb/Ih4lk1 ++irnERPuV2MNoAw3Fvn3/vwl/Jy0ZsQCBSXO54U42TcOXSwNLkYOJaomDiiuo61R +xj7jqijpnydwoFvEi84v6q/Uota3MijGMbzU9QyTX8J9OKMeCSUq0uVuk4ezebjv +/bwA/ax/qQRIrEHDOOB1LJ5JyLacK4+h5J8tMkEmWxEQv7MNokRLgbaePqv+tdf1 +gee4f2fSE3EXKFxjTO2wjLPXCrHSSI5gecsilQn7ZNxH9g2YUJipn9yj3ywMxQAR +AQABiQEfBBgBAgAJBQJOyVEXAhsMAAoJEF9prQh2ALIsrWwH/3s8uN8/gDnbcbTX ++7N/ZfQBXJZ+H9GGikmYRJE1xoOeEt9MOqZyGDTZfGM/qNKeDGfar7pcRQlMK/A4 +Nts5E6d1OX8fBkUBtYanyyjNLlT3yDjO6VaV0SCsgAzNjUZqc4lxS9atN6md5m6l +WLAdHghrXuV6LsiKOS+96htchoCvTvm7mcPI7w146yJRSyCC5+PybG3ult5Y6QAS +kwI3ZWB0u0PKUoqglwWngplu+0Fib2rxQvL32is4YrYaZ+XwoR6u/Bgv0ZvZiypk +17Uk17rDb/JfeLqDn7oW6Hlgi9KOLbRRIg7vwZVo2Ixco7aGxZp5c4zSfaPvn241 +v813ZcA= +=a3mq +-----END PGP PUBLIC KEY BLOCK----- + +pub 5F7786DF73E61F56 +sub 73F7734B17EC71F4 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=tVqI +-----END PGP PUBLIC KEY BLOCK----- + +pub 60BB45F36B649F22 +uid Dudie + +sub 84F2FF3B3DEABA7C +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE5ZBTYBCAC8yVhJ4YO3jk9rrhtrxWRdH9XSPh7Sd9SGwgxZDwL2xJnUPLO1 +dLGTkdCOLa/PwWwKF1CP78qeO92olKOd9nVpTyU9rpunbvLRgThdAvGs/v0+Qsyl +YAflbBX2jPripsTU+TgC444qIi+qUYcYX0koY/Jd3GUcAxCNwF6gJpD6vdi15Ain +Y/sfE7o6dborbs8hwXtYPZE82/tXrniGCXHzx2oft/XHyIRh+Vt2ZWEyI8w8BdlY +qQVU9WGindt2qhx9WWCPwXxMMgBOyJMgOUomIuQpfUW+rMDMhTjA+EVqX05SSehN +eOm07T3wM4Oxh4vIJOtFaLcZQcOba+jpApFnABEBAAG0GUR1ZGllIDxzb25hdHlw +ZUBkdWRpZS5mcj65AQ0ETlkFNgEIALbvYTK3AsJLLHts4LnvK1D6dwC+9INcJuEe +V9bCE89ikhIXed1mk2/EYKDtTuIAu1oSYSx8eXZyTjdW+yC55jUk5l/z6rZWDet3 +tQcZWvApf8IB6M5XBi3JYYE085XuDA+7ILgk1FILD7qMuBSwwZNoVZHfU+TFOFvY +uLHD8gPe8PU7fbhTlvQP0BTMe+fZGCz8UhleoTFk1H1ES3PXGWE4kOngaA7uy+4j +YWAu9LR0Sm9lT7LbedolMeyGki6BbW1ipkNgaskkGSKk71S9457KBXkYdbmlTXSn +09TOmQ98qHVOtR5PeiHeZMf9gohuJVSChjQbBo6iZlQ5EGEMb7kAEQEAAYkBHwQY +AQIACQUCTlkFNgIbDAAKCRBgu0Xza2SfIuAPCACd7BMrYXeEIXQnRsDKcX+oOzjt +z0610axnqLBmNM5u8nz7jNenbdsoj1AVCFNtlOLalFFR0oWcY2YxluKPuxy+epLP +Ycrx/N40arhpqxqGzypDnPHKHX9Njx7PfxHxjVQcCE0HdltavPe9kEgFMkhKvef1 +YjeGhEcIpwtJ4H92UBMGcS6dR4Nxo/4oQ1gX0rZ22PWxQNcfioNQi6wXKnhmR1Pp +wSByuD+MNlcH2mIOJsmFPJB6Tp0E8FzGV0Y279PsQgqFOZiEZpDmQOOeT8XKf83m +jtO7/UwIl/mcY8YcyfgoBx22SW7apOLe2xvMCwA9hAdkTVcT0i96pU0q1nfc +=CDTc +-----END PGP PUBLIC KEY BLOCK----- + +pub 6425559C47CC79C4 +uid java_re + +sub D547B4A01F74AC1E +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE3XFIUBCADcj1zw8m1evCgEMqxgOfl6L8y1tsYWsX7tVPvHEkYlXHrdcpkB +fGuWPrauvhBmB9sBkFfxzU98Ilz3Xk9pfISYiaMUk9Mk1ZxsCoYPVhxvOSvk5LgS +sviDzjYdZfZtskUM0sRmjmoQL//fVQbfLxJ2zses21za2VHuS3puUbdcm8+UIl/q +oyneDbzM7j2nYXXJPNXJOfvyVxi1+rsc7xcjMvAj5ievYlWwYlAIgYbAiz969NdL +RkoA1Wg+cQg+59k7Wvi6xwTfzMsO4jfkV2p24xn4fpcch9J49UhADh6O7XEls1Xr +80WjysMJWTOX1O2oTtV/BMjpI4gj08SgZRhzABEBAAG0ImphdmFfcmUgPEdGX1JF +TEVBU0VfV1dAb3JhY2xlLmNvbT65AQ0ETdcUhQEIALq5+uXjS4IHZBmOWOBSf6R1 +EnU4pUqEza0uwgIX5Xr2uSaaCMPCm5xrbtf/Iv45VEuR8zGKb8/0dV74me6nXnOe +qD27pkkliVE5nMPQnqKAUQmrA5aDR7Tzmey46Bmc+IFrvbWqiyA3yZwUpi1FKZR5 +VLEYhMGI0qOyoaa1NWjD3LDL7/AmQESe9QLCtT6QhNhmj/QWByRpmuIhayNyPGlh +5osFyiGgVcinlZE7x12uG76C1V7jo9eYrkjl/uHJHRqfB628oLubDFimKl1raYCl +RZ63jkbZBfC1fRYzxk6356mAxlB2OVDH3aYB97KKZkU8cX22IMawk4aBhCyhX8sA +EQEAAYkBHwQYAQIACQUCTdcUhQIbDAAKCRBkJVWcR8x5xIbAB/9HU+RuaFxAIVwy +SrAvBwycrq5qb850RU9+KgrKo8CSCKTLdmphgBSE3pCMr6A/Q1QtOUndbm7SSq+X +qODhij4FfUx0Kz669iPEVEZgZCausY7LH9aTmTJCRM+Ey2eM32Skz+ur0T812dN3 +iNd8HtC/iaJAoGFAnWRHetcH03QMEuogZp80NBg0CHV5Is8x0uh8JRHi8hWD1f6v +Vq9/GwbgRsDOppVa8Z2BgyHOsBDoec/fYC3i4iF8rHuuSGqajswzG9SnFN1zLcGh +LEUEOJzeDCANb1b2sJO2r9xEvfNcswj6ksY5lgItE1roCI61unkajH4ViHheqLZ/ +7wRm6eOF +=Tyvm +-----END PGP PUBLIC KEY BLOCK----- + +pub 6601E5C08DCCBB96 +uid Popma Remko + +sub 0AC07D0BBD11498C +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGBVUWMBDACXALXWXSrB2V95lR1L+i+sQsTQt8tCIgX0iX9UZ7Vw2K/lLnLw +WYtM3oTxYox4OdgkK9tK6771EdCH5wQtRdUQJjlsBfZDPMiGqmh1jrAxAugEkFyC +anVQ8VL1Z7uPeqw4UbtqA7Or/E0aOhF3zkkmhaiE9Yrp+I3KXWH4F0Cj3X9IUcf5 +Z93CPcEFQx7ajxSJ1xw/mSgbU4AtKZXUdK1ehnFAhH3rcMVW9paFSYaXD8f+vUbj +hdJOp3e9UYEFShsdwo2X0FRqI318ef3gPDpbTATyCaz6NMIybDgRGo9WOGwF+Ysf +snXwLU2UnT44kpAzHjFdjZhQGcY1w7d8yGNrYX4qw/RMPhmuVefuF2yodBtRxhWW +09dwNiIYFVuGS4S03vlnEfYZlhmRgvWZK9PDJXm0vE5GI7LdOKlqwZxvoznjGmUU +lscRU57DtrNlAjyXMZaGdNfPIG85B+ijJmIb0REHbszvG6csX4g1MiZ+i0WID8Jl +20YpJTUkkvIztXkAEQEAAbQeUG9wbWEgUmVta28gPHJlbWtvcEB5YWhvby5jb20+ +uQGNBGBVUWMBDACp9Vpf+IvWC5bHXRe6bYRYm4LO74f5GICC3cqfHNe0xzwnhDM1 +X8Kve87djZrWp/Q07yjh03iccddZsH1Wfme0b1Ue3UdrhYMuvvMQPI2k+IR7+wut +AsDlMPbyRcgnhaAO5URhn7PW0Hq2RxmRTPHrXzajEJUodT2VgjEC9DOD5cDAU2gc +SUJgBANOvIoPEQAMBsYMRgYUczjvr8wTPP3kuqBq0MhZrETpENIGOeNIDjhkewkb +BweOubP5FeAWeFCML/3LBLM1lA1bNaPJL2qAuYgSQcxvqYP95AesYDlLK8SWogkw +y9etGmegbWXYUWI2frTDK4H4XO1/H1iAqUknB0t+YMNBO5UidotTmJMq9ln26Hx6 +RqO1ifc1QRu8A3VIryCdapNGFUib/TBwF201WJPK1MfsIzQvS/HgVmmu7tzYrIVD +HYbj/RXEiiULMfPZE4PezahFO8/oHmvkR0KLOuwnOuMyWO/DWGn30Cdd8k+00zJ4 +crCR/FVfwrGeTacAEQEAAYkBtgQYAQgAIBYhBKpBdze9gFRW2zy93mYB5cCNzLuW +BQJgVVFjAhsMAAoJEGYB5cCNzLuW3OcL/15j9/sQ7mzMVeAQPQlhMaFhtrheMPyf +0eOorklsJ9afcBPsYPCtY33vibJzm43MiPeys+tW42B07i3wWnrcS0ZChgCHzGoq +nF1QRu+O+G8hZ7EARNXMu+GAmY2sxCdF6vJtgEY5tIM82I9dtMMlaqvx6hKsVox9 +YZkvK2yL4x0F/nVD1VQTf3zUvZNrrdrowIf3cIuBTzkgTE7FrfaLsvdBZC2sNYuu +NAY94zHsfqlppCGNjSB5Ig70S/YqPp1WYxU6yiXPRMY6qyNIO4NkAmtIJFEEDveY +bbpvMBYYo8vcVjEXuCOQWQuMbfnMWu70jCC/3E0zn8RebEI5kdXxk0D4+pDh3TFV +nqgi40dbwmHEv4p06IBCzaSoyzDfYH0Y5i9kZ4zUM1S9GEOtFwgEWxD80g1YU+Wg +Mr9k+0YfV/wXDbIPLodc1J9OCnwqrzJnxZiRO0q/xM719KVhsJqR0I9e5trpPrft +AA4fEAfsK22q/zZWGn1aDs1RyzdSprcN2g== +=CEC3 +-----END PGP PUBLIC KEY BLOCK----- + +pub 66B50994442D2D40 +uid Square Clippy + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGDoYisBEACqUDZnT4h6ma6XIzdC6KR++uDbR2VKdhCuv0Og/sHEKkm6ZbG0 +OFB8tAaQx/WlsoQyf3DlLfUEOGDai875Aqor3fbM+E1hrZbQNfsOySKEE52k7PYe +0qGWlnAzINuQaEuZwNw+pjZqPraMlwc/hwzJB8yFNHCv25pCFohK7KXvFGr5Fc6y +NHBp6pM3pnDQ1kbkloDr32YZY2LdrfdkRqwa9STNMcZtM724aaInValFpVGEHolF +dklo9MIsMI6mVHlxi6UwFSSLltUfTXGYY+rt2Q2sLNnEKzK1GvVhK996vrNWCvpr +cdtbTzGE3WK4f2knhqzlaX99OLmkM1ah+p2EkK7HgWM9oEO7SYpNxKe/F/QfRNRS +4W0aokPsEtfKCD7vQ3cRWQXdqFwvksilv+b6pcSrwfAsaCzVuhB3lcIra4MevJcH +ZEbPrfGMi5/MIVtLayglLHSPoZtjQBhlqo8w3nuADR/aFlIUZ6NGOwaz5yXIGVEs +6E1wiuILRAd7ecJ3Zyr/URHjawfHfKMM2tNCJKl48cScBMY61FJ1EmYzwhDw+at5 +D4pCk75eM5/t6VdYQ1cDWm7J3LGXEANMU5aSZMqgVnb4SQEmRxkW7oq3Z+GIkQQf +Sj4OK6Oi4cUpM7b0m7Cbcsoqb6nD27VKD3J5KTYEq3e+78h0VRjhoi0Z+QARAQAB +tCdTcXVhcmUgQ2xpcHB5IDxvcGVuc291cmNlQHNxdWFyZXVwLmNvbT4= +=cBgo +-----END PGP PUBLIC KEY BLOCK----- + +pub 66D68DAA073BE985 +uid Ceki Gulcu + +sub A1766BE5F812AC2E +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mJMEYvEGpBMFK4EEACMEIwQA6knc/2gtbqDhPh5EzrymR4Hwi1Xf2S0aqMopA1zg +IeZzBgSfL+4fEfpXL4eAzvrk29jIXSizDEOgFpw3PW3Om1gASxub4Jo6EQrRgOdd +OlJl1bajIRC4pAoZafDzhOb+FkjJ61lEJzJ6pQtG0Yi24QWDBfXHkSiQSbZFvcC/ +FTJpZua0GENla2kgR3VsY3UgPGNla2lAcW9zLmNoPriXBGLxBqQSBSuBBAAjBCME +AdqQOy84O/j7xo1rAaMB3jGHCn42wBJF8nMVZ1oh6WRN8d33JP0ojCpCK9oe3lyx +jZRvBsVkFhOF5lsb72kqR34hALXmZvhwFhzNoQlz4NuDLg6aQjAQEyiS7NqI2SVT +qbGoyIE6yg2ZLuv2svxk1dNlvtqtfOnmoeIZG3pybRRhyuIVAwEKCYi6BBgTCgAg +FiEEYCAKxK52HxYU1sRnZtaNqgc76YUFAmLxBqQCGwwACgkQZtaNqgc76YUkLAIH +aAcCM1niPs/kj3NEmFl3P9ivExlWa6Q45l8qPgitCLO2v932TElX+ux8O+fv0Ax2 +XJezAj+eMV+lYScyvXpmzbwCB36nuPmtsCJ31kYLXhN2WIJWPvPVesreI/GQUq0W +uAngfd6DOtCKYtNKP7xqDu/2bMU23cxGaRj2ToH4RfCClg1B +=rv9Q +-----END PGP PUBLIC KEY BLOCK----- + +pub 689CBE64F4BC997F +uid Szczepan Faber + +sub C0058C509A81C102 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGAofm8BDADhvXfCdHebhi2I1nd+n+1cTk0Kfv8bq4BQ1T2O85XlFpp1jaIR +70GAm2MOt8+eEXt/TuPkVBWnJovDpBbkUfYWxSIpPxJzcxWV+4WJi/25fBOq2EuP +QQhkqHQRECQ0CsogzsqI/Tn3FksiGKB7v67hAetM3KpwZ5IlG8chLoaeDf7k3P3S +fBWO9MFxYW/7K5G3vqARKXHvzq/jYiXziMDeWIKswwTPqfeDc89tsEdE6GMT6m2u +ECaulbHlzEzazSAh322/yyf/nfVZ/yZhK1y0MjvwpOhGxFbay5hA7L4bHAwR3qb9 +YGiPIL+K97TYY1G5+3X0TSvTIg4VsW5VDu50oB2iYK7uGE08GhT4uc73tiDlZm8L +BUwT/KtKT7g++LYwAMeZJ5+rfIKKxblXUN06vz9stylo1rNVhTXftuqqO+x5uVGG +KlOWzx3p9N3nqrufwuoQNvIMzCAvJZNm99j/Y/40wsrUkBxVBGNs6nEpQ6c5lvf3 +24Dfk3nY/7Fts1cAEQEAAbQjU3pjemVwYW4gRmFiZXIgPHN6Y3plcGlxQGdtYWls +LmNvbT65AY0EYCh+bwEMALVHwkeMzw/wcUboKcEUmmXmiGgwDn4xac47U9x75JgC +OqQE1+4Hxu5qULrPlCLLP1PDmD2PK/QUwbGpjjEuw4YxI6JjuOQ9sQa7HbzRVOmw +0kd0T4hr4Xa37D3E4oAxqwpeXcPsUWewtpjoqjLpTDBuaRpp/x3sFFmM9+s2ci4S +614yppuWqu4X/u7w5CbWFYMKl/N5aqK5RYYMAgPUqsI4J0NKwb5UszFuatFevTvD +MuwOf9LfW7kun13s0Z+/+hWGlNhk38ahIR8PSr4yT1pR271dUQKCTtZUFC6ObVAY +WAaEzrJ2XuJMnbHjpciv9WqaXFLpda7eE4TucmjU3+W29kWer9ts48EkD8Hv+a8T +BXXzK8KBi0ACUJi6uma1DWdUk6tqe2CniwirRzR1mWhKfOmQqr487pH5h0jMSPN8 +Dhyyuw4Ef2BLmTQmvbDYv9bwkeisskKjg108OoWOid1tbXudFdPQWqNc8FVPMlde +kza4cC4qBd+vjVcKHrEx3wARAQABiQG8BBgBCAAmFiEEFHtpGhkJdiSQL06paJy+ +ZPS8mX8FAmAofm8CGwwFCQPCZwAACgkQaJy+ZPS8mX9PxQwAn+LmPCqO6ig0fsgi +nOhUaoM2QX6A//IiFDXa2pY3bKaWf5LAYpuvRAyMsGPI3ceAnwfFSMXjktlssmD5 +bQKFisEuCuFQ0B+dlMO/+BZ1Id1Nldi8yKRTfcffgONO4kuKGKN7MKWPBX6/cJfA +pwHV7QubGEl/b/UNjPVFv34QCLU1ZFhVKHO582m0N94dwkwThaQQZX/op+cT2kSC +DWn7zl38KoYSy/6ThxKyIWKimiEpug0VeRHDoYw2NUyVvidj/F3jsnbEiNTH1Rpp +DzXuJbN7c/fxaAAhlAgxnt/hvrECPylnA98CPd1tBl8Q6IDcgbXmIa/jLS+Rqv5Q +xUNYlwhcFP9WxU8RwzxIHo9SiVRUaLcqit5eVI+eZbcL+TZP5b8wtLoKr199Ej2F +xNkL3+InFdjTH2Ir6RZpmqeY4NI6ujL41iUru20RzTNCAQA8jgmCMq9kDxaykpzd +SvFHnyijywCZB1jblPtxo2UqRO/qhPfqSkoVcpWmxgiPUFOr +=E7F7 +-----END PGP PUBLIC KEY BLOCK----- + +pub 6A65176A0FB1CD0B +sub EA8543C570FAF804 +sub CA890A5FA09CFD80 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFgMcBMBEAC/xcIVVOOh+F7S0OTzBlFH34s5fDbi6Zto469tZyW1peyWtXAZ +m+2jzFfeTCHaUQO3YjoTy2fPygS4tVD+ew4EAzMG5Uti4kwWZw0PYKz2JO/gl1JY +fKpWWkpKfHsGIFkfsOX6J83J4GVpaNJBUHsmcdep8YNf1nYDGpIZCxufihQXhuuK +x9BPm2SUdeyFwUFdxhGN4JdalxZo+x0pvQ6sKO1hQKK14YZXQxLUV043p3me9lVy +Ubld8kcda0edx3cyhilehib3sZPVhOm8s18GmjV5/ApPnehJN7SueivB2dzzFPN7 +mUwrslti0j2DmTdOImzcz0IT7zErmiV7xtgsgP8jgKEp2LF23VFXuWsKO2yNubQP +shNDKpYMMgJn0PfD5gwYl8FN9Yzj3OKA5wiJpgPjPl2PveZ/+rOS91bQMG1hFc3W +v9ZWSisJAZlNQlfyv36rD12WhwQLlupLo0zPlqp7e/i5ZJBPg4unbAYECtJI5Wqj +Ljhyd0j68QWon1Ripi8ruqXA9MUe7JMy39ZmF3/fLT4rBiHyRVpWkVKjzLlm0Ks4 +f3cNAPxn4FWeTwM+oUzEbpkNpE/swIbR05u1J2y0f+GS6X5t0CSTcHk1VIOnOiTl +wLzSEJe9hNkBuNJjwM9Cod7dbdorq6Qwd0ffPJoTw1SVkHMPwIjikzxU7QARAQAB +uQENBFgMcBMBCACSC8Tx2N3ZppqJ03AuDJrBOcNJU903XTp5l37lBl0JiNCDP4+y +gkCTUyz0/K5YKQYJfyuVmM5q0ydqhQ68nmrmlxqvFxRIug5VqaE7VWhksyNAOROt +xGi9Lo6AukKH2vK52Vh1uqRPmK44qtB1+bk8DE1YHuht00XB1Awu4ojIt3WKuRpM +/oSYfbsol82dPt1XpDvN1et2bxeN9qRblCp7u83NRmdvAGiBMRES6yV6n8XWpQFT +kRYf7wyVromOzz9m81dWAW5Js5QIvh3GMbFMS+2bnT+OVIrnCtJCw0TvTX3xZxyM +EuaCvYInCZA92frmpHwJMXau7/1u12zuHLflABEBAAGJA0QEGAEKAA8FAlgMcBMF +CQ8JnAACGyIBKQkQamUXag+xzQvAXSAEGQEKAAYFAlgMcBMACgkQ6oVDxXD6+AQm +RAf/U+Boj2/27Z310j145uPhh8w119XcwVqCpgSAUwycwQNWUjwbN2cbPtHcpRup +7x4XNPXKV1yYIhNVFiL7rDi1Zk/ZmIvPGIdtNDJBycrtSsqt+pDRyyF3stBvW+3C +voQTJBH3bNZCZZNFDv0suPNFalqzw1CSI/0QdP8fL7kzGJ1GAXD/XVDKPNy1VoCz +pe+JAbUKaDV9DlWAnnGdliLNsf1KFRMXg1rC6HfBKwW23XEY/eyC8ErR5pxG9H/s +Sv+zvsks/epx63qXzUnNt9TwRyQkfkZGCTm/Dod/uVjM5BpTtmsS88xC6G4apQEX +bzV8naNyk3mPJMYcVrWDk96SHz53D/4uF/b/g4EpIR7h3O9ZClCogXrRrglQBY2U +twwzSjb0coyZgF5igBZ5E64uMrt/kGBMLmVHkwUl8YdQmQrS6ju8lrTrd/7Xh9LH +/MOxXBMZaXw+/ZPcrH3aQFSotcL2CXmBNvv4OsordiJoTeoIIFo+Y/8VyOgrU4Pd +G9MC/jNy+61NcB3VzeyA6r6cLu8+7DXjBiy4M1JwEcRo3VpehuJyTPsVvQ8HTggG +Evrxqmv/C+4fAddB5e8SpPLs7r5wrBsg+iKpClBjDBVFp2SIg2Gj9TooQhhlTS1s +77HxlnT3X9m7tuww0ouPjbVb98nkEmueBAtEEao66YqxNXdWH10UKohxeZveCQgz +HafIiDnv2ILdxc6cxr5w6jEntbd0OpIC+V+3l99eZ4Jy5r1pGZYEsA3AzA3GedYL +UWGNpDQCIVTPjhzebAKd3VBIlyPfMtHYfrhhA+rKc4qPl4SNqypfU0xr1MuHvb2C +U6wYYASoeQfcqdxb0QNxqplfS+DOUCxotejo4YWbRsC0EoNv8YkpLahhlIQZjawr +maZtRTob07IKg7SsO2O90eNJ3MLhf/AUfG1RE0GfHyo5wWn8owwdqEXmn9cddvA4 +gqs8bFBV+ZngWKuF58xwHv6d39noOoj85DdEBot9wOetGljAKDBMGCXWM5lXplOe +M+oFs0FC/LkBDQRYDHATAQgA23T9HLJVBqU5MNuloA8KKv9SLoSx0WYZ64uDpMir +LrHIJnTaJjqXh4dM83GGcM8/h6b7f+MeHzhBqfTU7ywkH+jgBJuKMCW8/AWKRonw +aH+gpz4U7mRTAByKPh/x22B2ScYqXKgEWoR1/PMASJKVfQbtuKquoP6ZHpgzd4Vs +FNEp9lXCfBEyM0g3yfYVRSm8wpwZ7e/fgYv3t72qD4QwgFnpInF0poy28B8pgHpc +bdQiaUFB1hChLw6MomOgfkzs1Fjypv6/TwznP3jP51naYXnrOlZwiWhxghPh5WL/ +YnyG3KSDEgEFaI09/JgusrevaHsa1L7R7YxvCGFSKaM4aQARAQABiQNEBBgBCgAP +BQJYDHATBQkPCZwAAhsMASkJEGplF2oPsc0LwF0gBBkBCgAGBQJYDHATAAoJEMqJ +Cl+gnP2AOUwIAJeYeV1Dn8kNVQK9w7K6JtDFBDtCTfwo/Lh+fMoZHFAIoA4XZ5AL +thraTIM9/15Hl0IfL0WaxXaHj8uf2GH5ZLHNj3OYUX9AhmCra/EUJCpowaXaaSXF +VUyCuAM5IMfSpHRpslnhZlBDZ9gg9/8UbBEzn39DxNEEB6uAK1BLIqoH92ICR4m7 +mVCD5dG5k73wx7Zi6mSk8Z7/ezi4DiFznoJBOsAxSd0QvSlEKCy1Tm0yPh/McANS +l2BcmorVPEzEDPh5dOW8aA/od9x7ndHVKjk01hvKzZ4nfTXufeJxmpfpKpDVXBF5 +bvOYlMXlPQKpwJSF4d9SrJda7FJnTyQ7aEfdoQ/+NGaTPTfhNLPQGfrSSjmcsX/m +U8fo6by91OyaC5ghkIOF85Sl9ANJ+xMb64nAA/IH4e+qqcE1YOXvFGUvbD4YEZf3 +ewU4oGUty/iG8lJUS+ZBtMCDM6DOsKDIX3UN6oaAyGOUCYoPaHTxO1LlZ/1k0mCt +O+5Gc+gre0bDTPwkfA+upQylAd/JyoXF28sv1nz5sDbh0Uoa96sNEKsCHKBAPLFp +jpW4BwZyNrpQleKqVsEgTr7BWQEggKpbJanH1yx89LfMAsoqjQmO90gv2k17J22z +VoEemxTOmJ9v/JvooRpdfO8ggYH/PKORMyV4hTEMhtMdv6ySb27wWaTajQXChtde +nBZxT/Cjgo+hX7gpWqmY4+yh51+EJVFvmNCMPBOaYdWO7NYW0aAs3C3sqkYM1Cjl +9d64/GjXRpIl/OEzOca3Oh/0I35pDtwXChtSobaP6WDMzKygERAMSENsfAIWl2VR +JoJo8rNSAW/5lk2o4WYTww5VmsXRPGLIK8q3VyA1YLIIltSqKyaDMuthzS9W4XN0 +tInzj6iMTbll5BR9hivn1ra/wOw7J1slhBpPneQpBqMYyaepMiOpcn5FJmUXzIJk +g8QcdZ6tuTq/a3k+FTiuyndXJKywz933JlwaTw5RjrDqc5y+mC1OCYsB4Gx4XlnU +pjR9iVjH1oML0H5i1H4= +=MfhW +-----END PGP PUBLIC KEY BLOCK----- + +pub 72385FF0AF338D52 +sub 458AAC45B5189772 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +BemLeqNijrvozak+6IJUvgac +=NN0Q +-----END PGP PUBLIC KEY BLOCK----- + +pub 72475FD306B9CAB7 +uid Daniel Lemire + +sub 1723844CF9A045EC +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE7+huMBCADW9rfqKBXOqUSLCK5Klag5WqLFxAOddqEM7wTx/42XaIKjDiAW +gmFnV4XBKm/7Z4fwWq7+ku6NDYUjBpI4vcQ2hYJJ4SRWZHT5wWzOmqgznf+/Qwug +P7Ss3EUTRGX3LnhKhKN656XQhM0PutdsHQlUKjvnl2JOaKerEhbHCRxga/U/WWOT +KdobRO+x8v1scsrnUG83J7sTSaja0McmgUhKrhJqrgSk1Tod45SxprxOyp0cgATY +xjHrf2rkafBn7K7aFDe8a73iCJPWS77gxTZCZ72xkcnMLR0m7QI8TzFa4lRjTovA +QcTpr7jwjmyjA1+68peL6VHdVr0cdXm34mTVABEBAAG0IERhbmllbCBMZW1pcmUg +PGxlbWlyZUBnbWFpbC5jb20+uQENBE7+huMBCADS5gJ9frZF9KUorujSdK5GmZTG +75MXhQLLR3UOczqElryVfudgwHfofBymcuiPPfwSNWpWLeylgCxs49SOrNfh2r2m +Cln7ZO1LwDOoRIfD8xUA+TTc4qbQzo0xt6M7WdEiuhLdxUGnM8s4fRsjwvN7wvA6 ++PGvgaeWIKaP+S27jZaKbVSGqR37Vuj1JkbsZV1V4BXXOb0gyNsT6s/Hcy0owWtZ +kVBIgBanYui9J2uosgRMhHeEJUO05w6ehCoAkr2ktzj0QRDmljmbGjiHEsOcO7ZD +u2JQyI25bVUCk21S1CEdIfZ4xmiV5Rj2Lwxb7LCDEe+umc+W2/7j5RtR7RWFABEB +AAGJAR8EGAECAAkFAk7+huMCGwwACgkQckdf0wa5yrcgCAgAh0VKQHRwwov6YDV2 +1H0/h0vv934brwNWxIPz46WVa0xI+fmA0wYXg+1OTQBy7rqJHWoK0I3M6qjZvvaK +o77yUUtdye16gf/SxKPUwXMyTg98ojOvq7orvDF0ktE59stzayjUs3vrR7xDh/mx +pwiCN602Jk27vCCTdg0AHbu/CsK+Cu1rkqlqa9nQ0O0No0IiGJdDK51/ZluGBhhB +73xQ+jqgAf/g1qabG0Zyv2N+5tw+Alp9sqDsfyA119B6q3xKoaJqo+9h+zQ0vpZr +NMuph0EDebGxR9VMiO2F98dacAm/Qr8QTyRj4424rkCSK2VbZtvalewylY514xg2 +lV/Shw== +=qKIA +-----END PGP PUBLIC KEY BLOCK----- + +pub 7457CA33C3CE9E15 +uid Colin Decker + +sub ABE9F3126BB741C1 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFIXyRQBCADe285y3Pu7KzoKyP6wqeNXtvvuwMatAmPm5x/i+S8MlryqzsYa +x6twUmXV1yKjjtGrO+9fHvTOWBfSSP+fP9KTaTQYSasoJq2Mw4cQDy1i0zrxNZUw +N4/BiyjQA25sdfaOolhO0sFlZuTZpYy5wG72KkA1ygNq0L+8aBKhEF6zDU61YzCC +AxjcgTftgTeeoqkJtYa06lNz3jmJDN+zUQignfRa3ymoGtFHTzoXR9maE8RWDty4 +y+DY+8ibdGgSgKPZ0byTCDyNojgU1YTlADa/1/NY1ShYg617O1xicLNo0JEJlf2U +Tu4Ymql36+xSkYSISU97Q6Utgq27XMuZvDUDABEBAAG0IkNvbGluIERlY2tlciA8 +Y2dkZWNrZXJAZ29vZ2xlLmNvbT65AQ0EUhfJFAEIAN9NHRd2bYP/3CDi+n1ilSCh +ld0NR3DUBgS/AdqQ7IoAUfj7skyI/WyaMdV4uy6vRh5YgNg2g01nd0LLZR8Gf2Ck ++D6F88CdZaTxlkcxHV/dXMZ8yBO+0D6yFRZEL7Imsv8Ig4QXOVwfuiXEPk/Ef5Dy +9SdAVhcoErTGGR6BOGVVvexGtBwefsjMaOG0khkRbWIQ32WxfUFuAv5XBQ0ckLrl +KvYWUYhOlXg27GtFKH2EBBF0Z5ZWu7gaBFwSV0oLp9EWcD+C+WEwUSfBdqfRJtyX +vgf4kZdwdQ5caM8P2/Sdncl2l/LU1At2Smc+plr6zhIhDlLhlrzKGa16oARSBdUA +EQEAAYkCPgQYAQoACQUCUhfJFAIbLgEpCRB0V8ozw86eFcBdIAQZAQoABgUCUhfJ +FAAKCRCr6fMSa7dBwURMCADHrqwRNHkbG1QsXJr9oUK6KVkLsPhcngIhxRLlqe89 +omg9G7eGNauzs2PKsB3txotCFc7ROVNv/TAuSDYzkPos8G46p3bGesjfJb24zc6G +MT4RGIJoh1oNG1IciafIIHjp2ZJHRmEDwmvZG24OHJ+mlHLjaedtqlWu+zwwhH2V +ZrI/U3gW/x4imbk9UyyzciEIxrAc+fc19xl5PkUVcSDVC0cAqGpeZz8+SxFaf3Rr +0aGnSbeuHRjNupmoxkQOAey1ztmdWiCPf5RFfmFD+fENh+/xqYiGorYpcIN7UAsM +kvD5UHc5ZG2tTD41jM99w9Lm/xHJ9ks8gNwZESwIzr6ABKIH/1ulsflI216qPz5o +7uUxlTm8NfTyATfCUuZEDMYGOjDQPqQa8hFebqjWWYBUq2SlaKD2xMeEuEXV+M5k +88Cx6T2nvaZWMsrD7uGj+tTsFaKBGxP5p2OSEWOTETKKv6Cx7vcMTQmrqSFo47bF +KlNSs+aVM48UnQeFtTDyOhwa5jvtqtst4eQHwHWQ99BK0TEymNx0vF0nPjWA76CR +rfopOwXKdxJgoKq4MrxE92ot5I82AZBPeiWVJ+6wECeK/GoBIXZ5jEUqrQmmzIbo +WA5G5PMJ8egzLJNRJjTWHjCWrUTnwNcqaD4/qZxIlW4Lt0uvGlx6pKOJQ05u+9X/ +BzoVWrw= +=fJQM +-----END PGP PUBLIC KEY BLOCK----- + +pub 760F395DC40D55A7 +uid Dimitry Ivanov + +sub 9BBDD59B9EC621AF +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFSf+Z8BCAClJRKO/dDl1k55cP9BKpobsJEI6SInE7RDmR3NDsF4G2Hefo3z +UZl90EYkT1aYw5tE5c/SLsUE5LO3EmrGK91HFpYDdAajg+WEyS8UDPA/npFPvoRY +72rgPTZOd/i3b4k53A1h1OmXjBN9lKwsBQ1E35UDZVoWkEuZ0SlJoKv1/ckzISoU +11Opj7m+fmfCpopRpDHQ8hbRSqofhrOz3HJtUiY3FezcjatwHgEcgtVJjr872wfO +ZnVFx9uHMTD9gURQbGT1Wc+A/q4whEhatuI0DKcPrssVQ5Z9I1XzYlhgzegzJdk/ +KZq35XNYfwfPyZ7n5CxmNu9wk7z9wxe4VDTXABEBAAG0JURpbWl0cnkgSXZhbm92 +IDxub3RpZXMuYXBwQGdtYWlsLmNvbT65AQ0EVJ/5nwEIALQR8vdnJo6yAfIMlMCP +HGPyzDL2qGwZp/N+GbpJAJO9gJQzUvrL/uijy3Obw4umavIgr5GhbZcUffzFFU3d +Iuu77BuHH3DLOpdYQ0MQVYc3ZV0+2r7suaSsl0V1nmB5XBzIoKdowXGcybgDFH4I +lxP8d+cp8oppRO8tNJy/ls8Wj48EguiRcBTHm//kdOMQUZ71z/3VyPwNrn4pd+U8 +X7sIobqU9SsE9fFm4SDBpr/wkcxxZ6YgkYu+V5ZT2ESKO3h5LEiRKjmwnjE10mem +pty9zxIrtOuD8QqK1EDbbI6OgET36ajM6znmMDjmeEhsua8jc8XmBuS2yVy5p2yQ +EkkAEQEAAYkBHwQYAQIACQUCVJ/5nwIbDAAKCRB2DzldxA1Vp5GJCACXXeNkGdhD +cQBfUWD8HpaNPj7EWBzB8RkqvLv4iKDi7uirUxNOYaJ6x4LagX9JWUdmPLLanwQJ +PrF1Oos5T8XsE6gRXYM24yDN+tJj5H8KgDuhbXOyAFmFui+hva2+8RCnxq6i0qNt +3rp0ZlSF/6+MbcNx+t7dr+963QG2N6iu01xPJOpoMWHtVr6KRVaOwdck3zLViXDm +xOcUt3JhIGtKwRMO4mte4wmT6Ko+Nj4uy6tFjbTfN2eBins/1F9qLU4YJUqC4QD4 +8TvN2g+mJng83sC+lG2Wcyk5J9jaO2vvRRELwdplgBPqNmwVaQfPIVnJtxjuqGPv +22HRIgeCE7aG +=t42b +-----END PGP PUBLIC KEY BLOCK----- + +pub 7721F63BD38B4796 +sub 4EB27DB2A3B88B8B +sub 1397BC53640DB551 +sub 78BD65473CB3BD13 +sub 6494C6D6997C215E +sub FD533C07C264648F +sub 32EE5355A6BC6E42 +sub E88979FB9B30ACF2 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +c8LNGrDaCFdXnOdlNV/zT9VvBk/RkV+Tl/Lk4g== +=AP/d +-----END PGP PUBLIC KEY BLOCK----- + +pub 7905DE25C78AD456 +uid Protobuf Release + +sub DBC5123E2E98FEFE +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGSsZCsBDADJZoPoHGJNAB3sn/kFQ3zlj+vZ7OY5aWoH2nL3tHQYZvN/pJRs +8wu4Cw1ApatqLIaur6S6LR+s4xB7HxnMvpiF3NMwr6ZeZBUUTGEJbRgFhY9TqZam +CZJ/xPz/FevPhZn3/McqDGbjEx+G7hciUl0EwIOhanAQQvVYaWxDL+Pesqqh23U7 +Cex2NcotieICt7dWJ7SAM3TOSLP9OQd4scRvYLWqv6/vu/nQ68RwqaonR2QzxhUY +Uul7vR3iNRXtbnS31qIgCYWAoX6w0xHf6KUeIPWV21ZIUu5cg6kQr/sPt/OQuGS2 +nKk+InYtopDi6d7AUh8WI2TP7qAMIoRkhAeDEQ99DiopwFNPA/7M4g99AQfFSmp3 +acPCdeXXAZeDAqoFGFKTlqzg3FLWpGkubI/iXyHkpQfOXv4MtYuPGVNheBXGcWbf +XPjbkFYjkGIN2Wx4i7yf43hMCk6ArhswfgCcgoORI+DCVdm7ORID1PjIU2Z71EA2 +qDdFwdoOdEV42YUAEQEAAbQsUHJvdG9idWYgUmVsZWFzZSA8cHJvdG9idWZAZ29v +Z2xlZ3JvdXBzLmNvbT65AY0EZKxkKwEMAK4LeTj1dr8F9E98Up6y4AKHY0Zbeb5v +c/TzsJX6UCudzygYTbQnEcrPIcJ5TJV5leniAlxnqUz/qJxmpBtGCNH63c9+iJNh +VqJEZh9dbupqQn+mqtBvsPABbHU+C46TLebmOK4R99zgtxVlSYabJubuG2Mqnq96 +mutBUWKI3iY5j0JAMLY1DJesAGwAWP8gvUZHhd4LJN3iikNSTWyUE0Hnwm2VKFq4 +cxI/6qaCpztfuSD1y0JplSfmKRd+ecLSqhDvlMZkwigUpjCvF7iSaPvpxWdkFabS +frMeIjwbGU/fLV8ilwtPPb57X6Nrk9NIUdVa6ZbxiuIErIcp3JfgfUfy7wxcI/Uj +Mq1I50NOwizLVprZbmKv1P88bACmdon612pnDhhs84phJmA7fzQ/jAqF1JQ4Crdz +L+6g56Kkx1VlN3dSmPjuycjTzykuNwZ/Fi0Lj9Czg4LVp6peSsPWS+lp9h9tOSzt +lQev+GXiQKZTYt8JxvBPOkm0hd5M30BDbwARAQABiQG8BBgBCgAmFiEEGlXwka0o +wH+DH6RNeQXeJceK1FYFAmSsZCsCGwwFCQPCZwAACgkQeQXeJceK1FbX9wwAmLBK +Q8JljEwk0KqYxawrusWXwaH+1I83urf/WsOJYEkKoiQObsFGTuaolyln6ZHyF+gt +uKeWtlbvG6aXqv9XXcsVQG7NMGdEAy6DTNj77uBAXMWTxVpD09iVeepvWSiz7r7M +gzJfluNgGDOGKpkxxIjS8NnOAsK9uquyvBQa97I+YniarTkpnVWpgSR/7V3HHf6Q +2aCKL3ihdK2uIS4dIrFi+mVCt2zDad8U8N7S2Gv2VO/vBF+hIFCV788hLH9HeX3f +70E99X57hrVCh0MeColOIV1zwK8GLeV7bpr6x11x5cjiv27xky95WteyH5w9w/Xq +Tu0NQ5YyKX/0PUYVX3mLs59H7Wys6ANygWJs59JT4KSwb3pIEV7gWSwp3mWkstlF +m4Tq/d+gVF64ItrHylZg0WpHPv1s+dH6/tWcsBnkgR/OS33PkijQgvMW4imQNRxg +ymOZIduHXX1X+KzlRZTXvv4tSFnIQ0mWY1ySiOJQJS2WABVwFpFc8rECm6eN +=z4dc +-----END PGP PUBLIC KEY BLOCK----- + +pub 793FD5751A0F0780 +uid Block Open Source Releases + +sub 59EF9FBBED4216F5 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZzuZjRYJKwYBBAHaRw8BAQdA/FAoxAeXKY80R8UZ35NNr06SMFqC3Pv/LtP2 +sZS+7N60M0Jsb2NrIE9wZW4gU291cmNlIFJlbGVhc2VzIDxvc3MtcmVsZWFzZXNA +YmxvY2sueHl6Prg4BGc7mY0SCisGAQQBl1UBBQEBB0BxT6NyXTVMCaIjZGa2M+/h +z+xbicRNpHTpudCgcUvccwMBCAeIfgQYFgoAJhYhBB0hf4R17unxmrjda3k/1XUa +DweABQJnO5mNAhsMBQkSzAMAAAoJEHk/1XUaDweAmCgA/2x1ZfeK2BbKUoPDvSHk +bdks1K3OorITH/c1RHMJZe1TAP92RwDPmqy7YsZDKctpFeA0jhlWBNvbNTNkzRNu +yEjMCg== +=Eg1b +-----END PGP PUBLIC KEY BLOCK----- + +pub 79752DB6C966F0B8 +sub AC9F6F1991913E30 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +cJR4YXWx +=IK4j +-----END PGP PUBLIC KEY BLOCK----- + +pub 79E193516BE7998F +sub 9F7335D63326E7F9 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFWdcSoBCADK8j+0eVZKUGctZo/VaJ/K2Wppx4jEFgih8xiIWREQ9B3QEugJ +mJMWZHhrnHB+sjVx5No482ch6sVhYmC+VMyTdzepItZ8beYa0pnNGJnrFT+HcTOS +g21Ef5e6BRORNho2j9YTvxvjof29XxU4SJFVgffs48jGeJzN1EDmOz4OlZupKGU+ +98o+kMKCiFjcf6Vu03asuml97b2fMOJ09n+UQVlZbBR/Yo407ZLkL2Elx47Fz+82 +iO+M8w2qNnxT4PA/TLgaVzkVHaR/JIDlQQ4DfuyloQI1hBpMB8f60oukVr5dBGuS +1dPZ1H7td975sLegWoj7CCOFZXrDzYUXwwXPABEBAAG5AQ0EVZ1xKgEIAKJ2COl4 +DJyi+Y49Z/95FcSgPuJtM50qDLrStwdg94Hqk8xVpQyFlBDhBNNFNpgTD1Im0PaK +27uSGK21y0erdEDkSYh7nT9y8XbYyKlwmoHcwnsCdCn73aUf2uuXbFCy7uPr/Eey +gR/pQVbNGjdM/Hud/WWqBjwlWlpYsm2x8HK8uLTjLvcMgXT5bdKZKkO8vmHq14Bz +voXPbq0FWPcx1QUXtSdr6sxTjYrRZQ+QaE9/Oqc/LQYtFt9ieOp04EXrpMyVi27V +ed38WABVK6AO2HctsWCkNslagavTa2lDam3bZRISUQ0t1nkLOIrbYPqMjLpiHTY0 +m1JWw19DuxD9eikAEQEAAYkBHwQYAQgACQUCVZ1xKgIbDAAKCRB54ZNRa+eZjwAF +B/4rRTLxqv64tWpUV4S24cfPx5sAQZIEvMArv6CCDxGeFo0QfBHxGADG5qrZmxhX +P5S9kEsb6gHLFBhZVakXvlMfS4pUMf/OM7w+mhvdHk83xaTVB5IATku6aEWw4IFW +gkrg2Cosk3NO4RzZeImTjd1Uojml3/+K82yM79J9LMHNLNNDdSHMCP6T/u+LfrZn +x+D1I2cFAunFxzB0n7yJgDtoTzAG3d6yf2Tv2VcIzaHI0eX0I/zEjoJeYYlxJLgX +aKqA9JUyTGAsD4MDxjXAa9sXlzisXiJW2BsIf49wLCtDqbqlCX2ecLwA23ZnKPuj +4mp59atFr4X5kWonuyDxtrQd +=xV/i +-----END PGP PUBLIC KEY BLOCK----- + +pub 7A01B0F236E5430F +uid Inderjeet Singh + +sub C3E640F38D845FA2 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFZUsiQBCADGmoidvh3VvXWGdwbAtHPtDPKEebE/MfFVO+QTRbjJxphzKwAt +mxHruikafaSTnC9FWizj99e/Yc45YZHcnt5Htmy0a7DSOQXL37rrnieZxg86tYmC +4PxvvzC/s7xF8wmxDo4A+mRyoSF0NF/fQTZAr3ri5l0G/vntH7w8AbiiyerpLobW +/TqQn1tpMh7XfZZ+XqQKANVRECUiCYT4iJKWMqcBpLZW8aa+iYW8yCQ1xfmNXjrx +jpTqFCiQjvwCw4dDffNe/A1Dbq0wE6mw3YHW3OC1fnLiP+TEM9P9v02bZyem6uW2 ++krrToLTTHSqIGF9wUUF6S3Ikrw2EtJiRQtnABEBAAG0K0luZGVyamVldCBTaW5n +aCA8aW5kZXJAYWx1bW5pLnN0YW5mb3JkLmVkdT65AQ0EVlSyJAEIAL0+8UoJuUsC +3jDE60tmrApu/hK+dCbe5UJnR8z93aQ/1AfEX6So6JZzBlxID/HCOvRjJbauL6Lr +vw2xgSnrnOzRLf1StvBPASfJk1Zdo9LZon6Xofzg34qCLUQLkDyntgXQaYF3Yw/x +fiqqTC/yav29VTzKnf0Nri8aXGsHOycJ8nTO7I0p4xuRirFu7Bkvd7bK99/tDxtt +YkvUnG3BUGlr85UX4uODh3EcVcgVQteawYbmsf4F00IBoTAycutCOdbP2RAgP6kg +FxLcGz4zVqu93QjSjEdTegF1SUXcGpzvDR8T8zRsQbBCZ32A/UJqmx+EIPPFHNkL +ijDp+f5mkJcAEQEAAYkBHwQYAQIACQUCVlSyJAIbDAAKCRB6AbDyNuVDD2xjCACq +L670xI/26dWsz66ZyHQ2yJI7DNQxoiU3OZs2bfrRZxLpGP9Q6YWCehb+iucvmFFv +LZBoGGWzffmVBisD2Yz3mHtF3wLx+2zJXHt1Xz7H6W89M54T3qUhQTTV6pl5f5/J +CXK1DP9iC0y453ORY5B60byrGIUvBAv+qWXBPn3ECZ/3oEkErb5ZGof+gJjffqvW +RAN3Li0WBRj0ldXpJoP/YE8naDJ7UdPfzcnh3tnOTfUDvFer1Nh00ilMmf6EYznR +waN9whc9W/1HwvDeXrijrc6/1U7Hp1r5b1DddTtx6aHxpWrcwYw1yXGcm82fjXnR +domz6nBt2DF400YubAZR +=RPJ7 +-----END PGP PUBLIC KEY BLOCK----- + +pub 7A8860944FAD5F62 +sub C189C86B813330C4 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBEvxja8BEADAzZOup1X0B12zJsNsDvXVIwmM6bB+uhEsUHoFTvmsEVwRoZtn +i7Q0WSFoY+LDxbvC4Bg1+urCrUrstRJYRyF/pMqPYq/HokRlPjtrli/i3mUSd0zN +PGC5+qXvAlOck3GK8Hv05PsW32SlSczZw6PSDKh0natuM3hnb+vt+w2MXadXoSwU +EV6GtSZpj19vRzAwG/Zv+ZUDCBXVQG13mG7nr6+Q9+E0hJf8i/XZBcvTuWPy5niY +kzWDetDqNboFgCvBXYUw6dJZTS3tHhrXXp+W6hoSZFzYnRMG+xg0ls1z1ejUZkwO +mWPL7fr0Z/svSrOfyRxavKx1viKobEdnLwsdHIVK7TGIe5fQzR7PQgBgpMCueoMQ +NoXkA6GqPTuwS3pgNz2k/K+Bz3ICT9l09SHXzuGcB4GObF7fPDT/UK73Mo3sM0M1 +u68Q51i3fG92Owgy4Z/YXN/IgnAUrCb+EkLYIscSHby1voyvj2a/nIXajmldHqNX +9yPJhkIAij95VcsD4OUXonFbfqHuV7WqXBv4AhR/z+BndUbMbrlkn+r8dfL77rRY +63EGV3k8A6IB/WJScGveJsNRGCZLReff+UyvRkRy0jVVI0/G32ge13PbpPLGHoRx +LXiBSZ6Nuat8R4PS3ry8HKzFx6r2+VO082ptyLjl7e3yQzdVNshpxYxQZwARAQAB +uQINBEvxja8BEADfuM4j+dpNgMDDXGemxTG2HkQYiZNro/ytH+WOBZ962EgKHWt8 +RKuHD+69fHb4bDjHKFF8yVv9+okei0qK13SWc/+uRUVyLmn1xPX9cgTvjChfsnRG +JlioFZ3XxdQJ3vH8h/Mqb0yqxAgjoWYQIqIeAlE+7IwNYZy+LsuDD8OUUSbCN3zN +Q9E42Mo1IDwiMgHl6IQEWvYqjuICiu6nEA42bWuMQJuc7H7UxvzyD/Wuwdiy2gxA +HAtQMh0i9N2YcE0ZWd2ovpzSe3Dizx95pxUUsaQG7wpu3U+qvxCZjP+/XVNhkDvq +ROuXGw7B/5g/0OMORgR/nOpodXf1TFpSEU3uPLTwwxYPow2CoQ2X9787ojJODrZE +nQ9YdYU1ySX2Rqse7QHOu5Yf/Mnx4G3mNTLAFHYlzp/0sjaSRRzqOooKw9hUpqNY +kvh88h6QQLckdH9TKIHqJk9UiENIEv37XJaVsr1WSAvPeHusQoMS8k/A/1knreLV +OFh9AoUKG+2gjYs6VUR4f1epLEWLBvsBBwGwbXbwwOIb/0blrjp3h8yp50Tvy+T0 +hco9fQW1O1+50aztQCfVBIQ++/NVoQX7d5z2K6TEcRfIFoIMbANSmB/ZX2auSNIa +U31hVn4cuEOyENnLYH3XCELaGhce6lMEACD1J1m2i0Ttfr13NeCtppsGMwARAQAB +iQI2BBgBAgAJBQJL8Y2vAhsMACEJEHqIYJRPrV9iFiEE1vG8eGB4COyOn2lDeohg +lE+tX2Ih+Q/+OTpCunloKhRNiKfMe3hZLiaCeKkcc2c+jZI/9Y5VqJ92qbWeShW6 +nJ4/4wNdAUggyTwAaMV4qncYC360IzgaUEYvlpnpD0ES0xvIVzl25lJVLisJDS+w +g/hlL3fsIqlOBiGWYREW0T6zRwm4LAA26n3CPgnF6Esput1CT78aeOjldEaYYecn +2zycZxJJ/EgJc/MkooYZpkKzdyzlKwcVoEdSjI0sXMzgh6Xev81aAE0zG9eM5Ev0 +a4+sEygp9pCAN5JIemtWaVzvSezsoBcWmeveaKWVKzU2WwWF30Jh7J5vm08R7wka +/Arq20zEcHGbS26MlJ44ZQNZU6QcQcFrPkYjgD7x+a9InzLPzgsRW6PbOBgm55zG +iJOCmCiKlMhePzDOMfYo+AekglJZvWYt6AC+iDu0EvsElg0EBtoo0ny3azDAjJwI +5/nmuMQF80Pd7QeUpqeL0XZl608dHppdyxjKXvqtVe6UrGJdifmWwAOqLb7rcHmI +yjnWTNhGdnkbPsxHGrl7hsoSOgxSxgmMO+Vl74ueArTC1bD6JhB9j8KLDkx57Zal +DrxVxHJIMso7y7QkemJxib8JkfFsaOFye3nvehO6ohGnt42hqvBZWke2E/7xC8ds ++UM/HfWdrkQve6YiDHdF2x8pWC+ok+JbFn916yL/54nwMp3l9/9ITv8= +=CPTI +-----END PGP PUBLIC KEY BLOCK----- + +pub 7C25280EAE63EBE5 +sub 926DFB2EDB329089 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEPonucRBACtbhYckAoyz1tuSXYX4XiqGa5390gIMcxe2hJ+Ncx9o3zX09Im +f8PW27BnMrz7EIydgB2wphhjfK4vkNNtm5ZDWH/zJStsk1Fe7lNuuxs8XorX1+8D +bhhFEuc2B85vNf2o9Y4V5GFwbD+tFNy4u24n7zg6/VgE2WDvYJ8JRqCEkwCggyLj +ba0lsZ2XtSINh/W8ok+9f0sD/A8WhqBfDTEBuG9gnuCYXM0j7XBBPdPS+FXmmfea +zyP+URKRprLCdt0ThZAMllIxZJrkbv7aeXVpM6KSZ/XvvaFQ/gha4o4iJFvpoKt1 +Er2j4Tz/STKztHGsMt6pqfrMNPWovu4tLuLZQmojtbIk+IwmcYxMy99owH8oV1WC +U4HeA/9MlUxzmlmrQF7VLqFTGEEqQaEJqz95wNPj/t1DmI97hshPzXLD4zwKwa9m +qZJPStRHM0a6xW2dztF12aXhrmYg1gIGNnsHtq+t8ZhfINZUurSWn0m65WT5notA +15s6hwyDACHWWOgFQ9jmWuGDh0ZpiaBe7BxeTV+MsswY81sOn7kCDQRD6J8HEAgA +sivVzAfz34QE+S4WTXCuknmYiSEEnyTwk9awb52vrYlhoQ2t2EhRClc/tR6QbhNM +haMxPt1OYeutOvZN4q216IE2SwZzIDDTchYApP/brBdIDf4L/XGWFIqftCSn+vnb +0LAzYNVuNXtNwRni2q/fZ3g1wniVMbJ2MrJNt2VhLrP9K/ipFz7JCJittMngmmDF +7mEKhnrqBROLubFsUfNmz1qRC6PiEwyyCCdG+4m8fIiSyqna3CMkZr/UaVfxuGZH +WM8HYGmiQjafqeLqo8aSbWerzDYtF2+v4hAAt9eDwdgYy8oNxXEvw7Q+G5lix+6S +UMYV6NKLNUbBYffm9wjVuwADBQf8DbA7RpziZWLv7DHjR31AA5nnGEeud0dCRO8r +wfQNnaQvuJq8siRmU3uPAL2NwDgMaa0cT1xt7p4/8/RU0N9otVqnzkLMUTuqq/wt +QrQt0OWsEJRyxemWFwiL9ZpU4eTg49cfOQXjg2q3fbx9D1Xr6Bu/Pn7UDU8r9GbD +StGJ7R3Z0kkhtCErWnGNXbuqlVd8uEsyeM2HYpM76BmH/8vMg43lOJyyh6Id20ZT +n3HgWzRI5QaDJ1JYBhMuVChbTPUCcMox+qgiH4KtRIAjt+m3w0Axjsqo3EFPweWG +pRfqMyiUcESt4X/Z9V2Nf41NH+nQ74v3RvpP7EWKf9FfEtFpr4hGBBgRAgAGBQJD +6J8HAAoJEHwlKA6uY+vlN70An1dGrF5xPmh6P43U9+ZwJMtk18aJAJ90ff5E8Fsp +JCh/PZsbHv+eJN32qA== +=win2 +-----END PGP PUBLIC KEY BLOCK----- + +pub 7C30F7B1329DBA87 +uid Ktor Release + +sub 0588BC69A286FF16 +sub 3967D4EDA591B991 +sub 72FF58594F983302 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBF+TCd4BDACbIA94MfIWL0SpvZwBddXgx36Lp9GYOWNgGoQCWSvk9vaMrLaI +rEll0xnoP98CfBQYrVSAmHDMhSLBCjNB3V1Sdz8GRdOG7HUffF7Cqwbm3Fxo3H/h ++Tsrodv23NuvKsDpgglUL6nJy5e/FO8y9dcxLXRRVdPFDhJubi08SiUJy9FQbnfA +yb2LuTzXtjDmjEsMZpdpQUlQkk0xNDkrrq+2miwxemVd35cnVQCFP0K7c4T0ksGg +Rf9A2r45DBbPfvwTL+ZbrGtCssUpCneWhPl79UsMxeY+vJjEggqqqRqbHRn6nOQd +3gKSaEqdALZURPzvkKxLUeUUtMk/tkFdsNe/ea7edk6G3MI4dbUY7p0XLS54S9cB +1JUAHNEFtuJQKGWNuwWO58Yun1EBtOdUEvnIIoQ+CIN/XeKrnEIXE3LSblB8BR3H +bqX54BMe9AzsmDQtc5pUOm2pfvCoiv8xFXQznBg24dGqo2A/jMoUnGj6oRj7k8mt +i9AdPLigldr0S0sAEQEAAbQhS3RvciBSZWxlYXNlIDxrdG9yQGpldGJyYWlucy5j +b20+uQGNBF+TCkEBDADafdzCGQlmG4e83+VsqAVCmiO1SlVkfwfgXpuXdnLx+rDz +f6FgkIwUcNwkBpTCQF3i457Mt50kKW4XIV9/uzSYM+VohUn273HmN0+2iExW0jW5 +LzxQf0jCnbPDnnfjc2qZ6B5ySmVks8zwsv9vLz6rcK3+IDJYMlTHLQaH+if2v8vz +MJ5r5DowJJOJcxhFZCBThXpWl1zAhpnv+Fwb9sNpoXfANwqzhpSi9PwDVqaw9at9 +fDRZgqlKqdIt7mlUA+Jl4jLe7t9zBquDuNeKCST97IdeTXV/NOGoVkp8pdLyEzQx +xdaCiLDdl8CaN/JVg9Jj/uwQRVq4KvRaUe+jMdQIpYu4RcHPQMkPkLXO5J3kSvk2 +cjtibogiN2HCPpa2G9H9Ar1TKKn1e5U4qy/fDryR11GVlEdFxVsugplbIXZLDzeA +FvEiFWVcMSINTnsKRp8W5yvvd58sEI+WbMLwym/825oRs1VocbTIfqjSmD68N/Ax +y7z0Vo3ZsUREArFvADUAEQEAAYkBvAQYAQoAJgIbDBYhBDlMtDbFaRb8Ae6kp3ww +97EynbqHBQJjUcQMBQkHgSDLAAoJEHww97EynbqH4rcL/0xgE9PanLa6WddRuN1k +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 +XwCWdYzvSwRp +=Hi5G +-----END PGP PUBLIC KEY BLOCK----- + +pub 7C7D8456294423BA +uid Henri Tremblay + +sub 9842FE565AA0601E +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGiBEvsZw4RBADH20nX+H1xvMBYmXRj1Aae4dRr6Y6qI7QRWHO6Z7/dxr9bk/NN +Yjq5KsVOQxZzloVdtqx75rznT7fZq98g7Nq9IeEtB6k4tnh6XQLhljJMk0a3mzdt +q3VzdxeVbwCaPJ0zixv8XPTAH6MpRJUvP9XjzxwaYHrjwcQ1LslW4TrIzwCgi5rf +jChLCyKcaL05gqUjl4lmefED/iqOwYZw5pJ8+X+OHUViiOB43wsJt1brAhPj4KgB +ODStcE6WlHFKi7YzcYNLzYMebSGYn6bj65b3qNf6rybWD1hGUFK4122Q7+HCH9Ic +J+rr8HwjGFo/yxI0/mkyaF0BthXYPy4WtdsdTM2kgx8Zr3Q2rSt1jBPuV3q8d27z +FZMiA/9cWPkRx0RfAJmBPKmKkbBkEtBbNau3G7MY1OEAkEkRnzmnyyjr5IP84A7K +RdjTCvkbiQrOQH00Ki4sHIg+9Xv1gDg1XLkFDzRARKA1TxjL0OeS4RWF3iia7Swk +MOnTdhR50pjb18W8kB4mEMZY7duP4nwDfQwHMwbFZGHrjImaurQpSGVucmkgVHJl +bWJsYXkgPGhlbnJpLnRyZW1ibGF5QGdtYWlsLmNvbT65Ag0ES+xnEBAIALYa2xQw +2yBqve3W19WRMDRqYyC3XrpsWc2gnOT4JXRxgPOky9lfYj6TjSbb+/wrK9XP67x9 +CAPwRbvtCnXvVD/s/ScnJnyaSLHdkLcX4Z/UePk3dFbTfTTZKbdfiXE4W88kKuFF +PNrgSsEhv8M7mziOBZ/u8qSLjYA5KitEkyC97nChf0Ve+z7DgXix5AhiiFYVFH2N +Q+dQVmSigdXn2lSuzy3Z7IuuJQIW2nsQON3cFFLVcEDrw8yVbZMnFdiDMF8Mh6J5 +SvuL23MCB9gQFOOiyGcvlRWiPpTTZkffsXlh+PaCWJzFiF4n+Ec7ztWoNiisJRtr +GnLlU7aVlaimsgcAAwUH+wRpeDzirZbgG192vyhp19WMomz09nuKXTsamyk8LkXO +yCyCvy1XHo1bi3fnjhdUJo06CO2N6o5c77WsUnpn5MWyEKXbrNEshxBABh/6ozbk +7PGcBJfbTz6ymiR2yfZEK5Qz8JLnwNDQwF0xO+lIXBz2NA3spAChMNq2rxKiO2NJ +10dlkpkSxQoZkPmjU5v/VBiPyKEXUv3YKNXmiw/+0SxXi+bRg3vtp4/geo/udp2R +JmkQlllOzvY2C9XELuVk7q7Z3gi7SKnoS7I7lc/YoQcrGLlHEzlf+ltACHJUKDBI +hUfSsr9KMA5Oi7V9g4ImybgL7y5Z2o7IhpuZ8lYdxtSISQQYEQIACQUCS+xnEAIb +DAAKCRB8fYRWKUQjuol1AJ0XC8Ne5QYXv1nChUZMxE0sWiXIyACePz9TTTp/3knC +3cdu7I3u6t7ARDc= +=xYLL +-----END PGP PUBLIC KEY BLOCK----- + +pub 7EB97D110DFADD60 +uid Niall Gallagher (www.npgall.com) + +sub DC0B7E986BD7398F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBE9/RAsBCADI/pVIFcoLmbq4LCKkqeN4i5xgGKsuQsDAf/ndFkILDUA2FaPN +7cI3EvZacWnWUA0QkkKNKpajU2OjjQlu4IyBosJht3VMtD0BJ2nL8eIDvwO6L8TS +2RRGMnMaDUc91NnoxKs/7VlQ2ySk6Cm6lH3t8KVkwaJdU59lAH1ey9UKhYyvRQuT +htenl2R63lyyDe1ZLMAlmQXi4RcCWOO+L1emChNv0q0Fsir+7go9ZNYUi6pmIEva +jKXM8bo/VtRIHrS73DsH7BVVCURYoBWexZWlRdb86KSE993dRXLvFPy5JzlRM+eu +mUY3CMKxx3nLaDN5qepf1nGzMW88xjq4z4rhABEBAAG0M05pYWxsIEdhbGxhZ2hl +ciAod3d3Lm5wZ2FsbC5jb20pIDxuaWFsbEBucGdhbGwuY29tPrkBDQRPf0QLAQgA +68HLImPvBSPnMtjUHczE+gccsVWzLEsjVYSBcOUi1j67KQHbTPcHAqzYJl19t4FA +N/yU1oOjuu/4GKVni27y8NGSavzY5elTZ22lqUqgqT6DjoOG2BTLHuOiNRIMqBmD +Gy41mEq62C9I107pqJnnbARmde4646kDiaf2vkF1BsnBx0Dp93re2eJq4rkAf803 +fDvA8iyk5uDFiGg3f70JAu7ZCAKczglD0WUjIiO5Jxncz2sWiO2OuVgdsTuZf+9T +0aODKua60Z7CLn4ZK4ZpdibbOEp66XLeaGuy5HPInTTsr4UnT2kvor/AmmPKOryp +9oBFnPvf5+wREwlQN2h/PwARAQABiQJEBBgBAgAPBQJPf0QLAhsuBQkHhh+AASkJ +EH65fREN+t1gwF0gBBkBAgAGBQJPf0QLAAoJENwLfphr1zmPJtcH/RJ5ba5m0Obq +BGbcJpJwhEjpB6tCOufdzvvJGAMMAuH0Vs5kXrASIJPyVgJ2ab4txg6U3DKIfxnE +IGjfdH9okl/oHRYrI/EDMN0PnIkE1JidhVOEOj3UWaoLUS8vvobKq0XP8B6J+P4q +NA5L3cPlBBtH7yqzVNavi6ljJcsJH3g7L5vJDQyw+xxfOvQq66y4lcO8ptAqB+nw +oHfSsfRKQQgT+Xlp4lG+acf+Kc0bLjWWUnBRgJfkhbGPVYHQ/QfnxbuLvlqohive +HEV+d/PxCwUHq4EtLC9d8V3ADCZgb3v9YE03abItwg7tnQBd/LuJ4qdOEbjAWI4c +crfZTmD74BlsewgAsOy6q3qjZwBwDjUHc4+Mq9ZwaehZ4Lau9EjyrOPZw1kSCWc7 +rcxlTbPa+2xUrv382Zc7KovIMmEos4RJ1gaFfXvreGKzCOrlsA4knXcwp9nywjZH +2Hd+p7l+/EB2YA99rMglXl/5Nfs5k2I1bTqfOb2eOXde5p8Akh1exvuIyJmRf/pR +eRA/8dXKCE/qG+KZkJOh8H9gD/8nX8n/5Qfm6Ff0iFF3mdBLM9QdBa36/qowBYQz +u25YFH0NKiziF0Y2r0pd4NQhoAirGdamVPfWKzvHhZ8XWhklr1Ve+3X9NqZIOcXQ +x1rrD3BL6LwVJtHcfQxuT3IRDnjnYXZAUOVAlQ== +=NFKb +-----END PGP PUBLIC KEY BLOCK----- + +pub 7FE9900F412D622E +sub AE6B5325E74ED034 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFnyVlkBCACe8zGkIlDV0dUKmk9PWe2Hw8qM9DdPbtpUOpmUOidGY5svQDL3 +eqvHk85TbxqFEe3Qbjjt+R+iApFuXy5kmueXTvwCm7nAU+k/pZtPuzHyhDs3iFFH +8LCI/dOpd04LXLpuoeLCjBqPlOM+Pxiiu9h9tEnJaJzuXcw9SY3I/puj7qIEwxsJ +W23gdPtYij9If1ht9gtTsDq9s7VbCM5vL0ofM8JVPilnE4oWuw9hjgIfT/QotbuS +wPo+1ExZUfaKYPvMxi2kY3LZU3hlp6P5AxU+eI31yaYjtL+0lu66jTD6s8lwmF87 +QqjjxiHwic05//tp2Pk3PIZCoQurfEL6ZHhhABEBAAG5AQ0EWfJWWQEIAKT/0agk +rbcbBEoXyTHiHag6RvreY/sNI+ayAEYLG+EfTigT1GD5I9YpOPHsTRcXK3IK4Fif +0GjIZU+njlfmThW+Pqw0mxK7JbsTEl/UADteprSajW2OxQ9J4BbKGu5kJNocaadj +oKUDcGzLgTULywLRRqlb100B/rpIOnSNZ4SRTfrp9rRFl7HP3acgMJNC/hhmRhE5 +frc/pJ7uE22ie2YoDolqMP7jo4saA1WB1Ipmk7Q8zIPxUyDlFQ3w9cepnryAnWKG +NQnj32xcW4BpcpzGRtv5qMPp9g9DaPLhLW1ClQaYEoB2+2Ex6cRKGbCTqWxM3zrI +fmr21M3AxaNNuoUAEQEAAYkBHwQYAQIACQUCWfJWWQIbDAAKCRB/6ZAPQS1iLuee +B/9o8PAlAbs0gIi4y579CVYJQbjNbYVkvWIIoxiF9WfPb2Uz/kAbhjRKVxGSASKC +LMFBSR4tUdqPehsuXGvUjnKuNo7AP9+u6TeugLWmL7WLSy/T95km/JU3dNremPNg +NP9DDT0CjmnLseSwmy8Azy0hvDmGebGrT3Uzz/N2AiL8ffxAUFYo1ho+QLpYzFa/ +Qzjsq21x+/UMEX82awzO2zjkEOgT3wJUH/kzZ4tVWtseWPhCqksNN5JjBxNvslVI +qs1vKnZazbuXWg2Ex7HO0GK8fPU4vEYljyh2sK27ErckyWUN2Wf8cd3CunqHtINF +3FHqWVfvjMh1y1lWyKLx4ke8 +=aHTI +-----END PGP PUBLIC KEY BLOCK----- diff --git a/gradle/verification-keyring.keys.license b/gradle/verification-keyring.keys.license new file mode 100644 index 0000000..67f06b0 --- /dev/null +++ b/gradle/verification-keyring.keys.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml new file mode 100644 index 0000000..f77a341 --- /dev/null +++ b/gradle/verification-metadata.xml @@ -0,0 +1,22485 @@ + + + + true + true + armored + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/verification-metadata.xml.license b/gradle/verification-metadata.xml.license new file mode 100644 index 0000000..67f06b0 --- /dev/null +++ b/gradle/verification-metadata.xml.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..8bdaf60c75ab801e22807dde59e12a8735a34077 GIT binary patch literal 45457 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 c82=K%QXLKFx%@O{wJjEi6Y56o#$)Bpeg literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.jar.license b/gradle/wrapper/gradle-wrapper.jar.license new file mode 100644 index 0000000..3f0600e --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2015-2021 the original authors +# SPDX-License-Identifier: Apache-2.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2a84e18 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.properties.license b/gradle/wrapper/gradle-wrapper.properties.license new file mode 100644 index 0000000..eb75407 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..ef07e01 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/gradlew.bat.license b/gradlew.bat.license new file mode 100644 index 0000000..d4790f0 --- /dev/null +++ b/gradlew.bat.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2015 the original author or authors. +SPDX-License-Identifier: Apache-2.0 diff --git a/gradlew.license b/gradlew.license new file mode 100644 index 0000000..4523ac1 --- /dev/null +++ b/gradlew.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2015-2021 the original author or authors. +SPDX-License-Identifier: Apache-2.0 diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 0000000..f177497 --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,19 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + ":gitSignOff" + ], + "timezone": "Europe/Berlin", + "labels": [ + "dependencies", + "3. to review" + ], + "packageRules": [ + { + "matchPackageNames": ["com.github.nextcloud-deps.hwsecurity:hwsecurity-fido2", "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido", "fidoVersion"], + "automerge": false, + "enabled": false + } + ], +} diff --git a/renovate.json5.license b/renovate.json5.license new file mode 100644 index 0000000..67f06b0 --- /dev/null +++ b/renovate.json5.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/scripts/analysis/analysis-wrapper.sh b/scripts/analysis/analysis-wrapper.sh new file mode 100755 index 0000000..47e4225 --- /dev/null +++ b/scripts/analysis/analysis-wrapper.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2017 Tobias Kaminsky +# SPDX-License-Identifier: GPL-3.0-or-later + +BRANCH=$1 +LOG_USERNAME=$2 +LOG_PASSWORD=$3 +BUILD_NUMBER=$4 +PR_NUMBER=$5 + + +stableBranch="master" +repository="talk-android" + +ruby scripts/analysis/lint-up.rb +lintValue=$? + +curl "https://www.kaminsky.me/nc-dev/$repository-findbugs/$stableBranch.xml" -o "/tmp/$stableBranch.xml" +ruby scripts/analysis/spotbugs-up.rb "$stableBranch" +spotbugsValue=$? + +# exit codes: +# 0: count was reduced +# 1: count was increased +# 2: count stayed the same + +source scripts/lib.sh + +echo "Branch: $BRANCH" + +if [ "$BRANCH" = $stableBranch ]; then + echo "New spotbugs result for $stableBranch at: https://www.kaminsky.me/nc-dev/$repository-findbugs/$stableBranch.html" + curl -u "${LOG_USERNAME}:${LOG_PASSWORD}" -X PUT https://nextcloud.kaminsky.me/remote.php/dav/files/${LOG_USERNAME}/$repository-findbugs/$stableBranch.html --upload-file app/build/reports/spotbugs/spotbugs.html + curl 2>/dev/null -u "${LOG_USERNAME}:${LOG_PASSWORD}" -X PUT "https://nextcloud.kaminsky.me/remote.php/dav/files/${LOG_USERNAME}/$repository-findbugs/$stableBranch.xml" --upload-file app/build/reports/spotbugs/gplayDebug.xml + + if [ $lintValue -ne 1 ]; then + echo "New lint result for $stableBranch at: https://www.kaminsky.me/nc-dev/$repository-lint/$stableBranch.html" + curl -u "${LOG_USERNAME}:${LOG_PASSWORD}" -X PUT https://nextcloud.kaminsky.me/remote.php/dav/files/${LOG_USERNAME}/$repository-lint/$stableBranch.html --upload-file app/build/reports/lint/lint.html + exit 0 + fi +else + if [ -e "${BUILD_NUMBER}" ]; then + 6=$stableBranch"-"$(date +%F) + fi + echo "New lint results at https://www.kaminsky.me/nc-dev/$repository-lint/${BUILD_NUMBER}.html" + curl 2>/dev/null -u "${LOG_USERNAME}:${LOG_PASSWORD}" -X PUT "https://nextcloud.kaminsky.me/remote.php/dav/files/${LOG_USERNAME}/$repository-lint/${BUILD_NUMBER}.html" --upload-file app/build/reports/lint/lint.html + + echo "New spotbugs results at https://www.kaminsky.me/nc-dev/$repository-findbugs/${BUILD_NUMBER}.html" + curl 2>/dev/null -u "${LOG_USERNAME}:${LOG_PASSWORD}" -X PUT "https://nextcloud.kaminsky.me/remote.php/dav/files/${LOG_USERNAME}/$repository-findbugs/${BUILD_NUMBER}.html" --upload-file app/build/reports/spotbugs/spotbugs.html + + # delete all old comments, starting with Codacy + oldComments=$(curl_gh -X GET "https://api.github.com/repos/nextcloud/$repository/issues/${PR_NUMBER}/comments" | jq '.[] | select((.user.login | contains("github-actions")) and (.body | test("

Codacy.*"))) | .id') + + echo "$oldComments" | while read -r comment ; do + curl_gh -X DELETE "https://api.github.com/repos/nextcloud/$repository/issues/comments/$comment" + done + + # lint and spotbugs file must exist + if [ ! -s app/build/reports/lint/lint.html ] ; then + echo "lint.html file is missing!" + exit 1 + fi + + if [ ! -s app/build/reports/spotbugs/spotbugs.html ] ; then + echo "spotbugs.html file is missing!" + exit 1 + fi + + # add comment with results + lintResultNew=$(grep "Lint Report.* [0-9]* warning" app/build/reports/lint/lint.html | cut -f2 -d':' |cut -f1 -d'<') + + lintErrorNew=$(echo $lintResultNew | grep "[0-9]* error" -o | cut -f1 -d" ") + if ( [ -z $lintErrorNew ] ); then + lintErrorNew=0 + fi + + lintWarningNew=$(echo $lintResultNew | grep "[0-9]* warning" -o | cut -f1 -d" ") + if ( [ -z $lintWarningNew ] ); then + lintWarningNew=0 + fi + + lintResultOld=$(curl 2>/dev/null "https://raw.githubusercontent.com/nextcloud/$repository/$stableBranch/scripts/analysis/lint-results.txt") + lintErrorOld=$(echo $lintResultOld | grep "[0-9]* error" -o | cut -f1 -d" ") + if ( [ -z $lintErrorOld ] ); then + lintErrorOld=0 + fi + + lintWarningOld=$(echo $lintResultOld | grep "[0-9]* warning" -o | cut -f1 -d" ") + if ( [ -z $lintWarningOld ] ); then + lintWarningOld=0 + fi + + if [ $stableBranch = "master" ] ; then + codacyValue=$(curl 2>/dev/null https://app.codacy.com/gh/nextcloud/$repository/dashboard | grep "total issues" | cut -d">" -f3 | cut -d"<" -f1) + codacyResult="

Codacy

$codacyValue" + else + codacyResult="" + fi + + lintResult="

Lint

Type$stableBranchPR
Warnings$lintWarningOld$lintWarningNew
Errors$lintErrorOld$lintErrorNew
" + + spotbugsResult="

SpotBugs

$(scripts/analysis/spotbugsComparison.py "/tmp/$stableBranch.xml" app/build/reports/spotbugs/gplayDebug.xml --link-new "https://www.kaminsky.me/nc-dev/$repository-findbugs/${BUILD_NUMBER}.html" --link-base "https://www.kaminsky.me/nc-dev/$repository-findbugs/$stableBranch.html")" + + if ( [ $lintValue -eq 1 ] ) ; then + lintMessage="

Lint increased!

" + fi + + if ( [ $spotbugsValue -eq 1 ] ) ; then + spotbugsMessage="

SpotBugs increased!

" + fi + + # check gplay limitation: all changelog files must only have 500 chars + gplayLimitation=$(scripts/checkGplayLimitation.sh) + + if [ ! -z "$gplayLimitation" ]; then + gplayLimitation="

Following files are beyond 500 char limit:



"$gplayLimitation + fi + + # check for NotNull + if [[ $(grep org.jetbrains.annotations app/src/main/* -irl | wc -l) -gt 0 ]] ; then + notNull="org.jetbrains.annotations.NotNull is used. Please use androidx.annotation.NonNull instead.

" + fi + + bodyContent="$codacyResult $lintResult $spotbugsResult $lintMessage $spotbugsMessage $gplayLimitation $notNull" + echo "$bodyContent" >> "$GITHUB_STEP_SUMMARY" + payload="{ \"body\" : \"$bodyContent\" }" + curl_gh -X POST "https://api.github.com/repos/nextcloud/$repository/issues/${PR_NUMBER}/comments" -d "$payload" + + if [ ! -z "$gplayLimitation" ]; then + exit 1 + fi + + if [ ! $lintValue -eq 2 ]; then + exit $lintValue + fi + + if [ -n "$notNull" ]; then + exit 1 + fi + + if [ $spotbugsValue -eq 2 ]; then + exit 0 + else + exit $spotbugsValue + fi +fi diff --git a/scripts/analysis/getBranchName.sh b/scripts/analysis/getBranchName.sh new file mode 100755 index 0000000..63fb313 --- /dev/null +++ b/scripts/analysis/getBranchName.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2017 Tobias Kaminsky +# SPDX-License-Identifier: GPL-3.0-or-later + +# $1: username, $2: password/token, $3: pull request number + +if [ -z "$3" ] ; then + git branch | grep '\*' | cut -d' ' -f2 +else + curl 2>/dev/null -u "$1":"$2" "https://api.github.com/repos/nextcloud/talk-android/pulls/$3" | jq .head.ref +fi diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt new file mode 100644 index 0000000..9dc6fc1 --- /dev/null +++ b/scripts/analysis/lint-results.txt @@ -0,0 +1,2 @@ +DO NOT TOUCH; GENERATED BY DRONE + Lint Report: 99 warnings diff --git a/scripts/analysis/lint-results.txt.license b/scripts/analysis/lint-results.txt.license new file mode 100644 index 0000000..eb75407 --- /dev/null +++ b/scripts/analysis/lint-results.txt.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/scripts/analysis/lint-up.rb b/scripts/analysis/lint-up.rb new file mode 100644 index 0000000..c52b60f --- /dev/null +++ b/scripts/analysis/lint-up.rb @@ -0,0 +1,191 @@ +## Script from https://github.com/tir38/android-lint-entropy-reducer at 07.05.2017 +# adapts to drone, use git username / token as parameter + +# TODO cleanup this script, it has a lot of unused stuff + +# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2017 Jason Atwood +# SPDX-FileCopyrightText: 2017 Tobias Kaminsky +# SPDX-License-Identifier: GPL-3.0-or-later + + +Encoding.default_external = Encoding::UTF_8 +Encoding.default_internal = Encoding::UTF_8 + +puts "=================== starting Android Lint Entropy Reducer ====================" + +# ======================== SETUP ============================ + +# User name for git commits made by this script. +TRAVIS_GIT_USERNAME = String.new("Drone CI server") + +# File name and relative path of generated Lint report. Must match build.gradle file: +# lintOptions { +# htmlOutput file("[FILE_NAME].html") +# } +LINT_REPORT_FILE = String.new("app/build/reports/lint/lint.html") + +# File name and relative path of previous results of this script. +PREVIOUS_LINT_RESULTS_FILE=String.new("scripts/analysis/lint-results.txt") + +# Flag to evaluate warnings. true = check warnings; false = ignore warnings +CHECK_WARNINGS = true + +# File name and relative path to custom lint rules; Can be null or "". +CUSTOM_LINT_FILE = String.new("") + +# ================ SETUP DONE; DON'T TOUCH ANYTHING BELOW ================ + +require 'fileutils' +require 'pathname' +require 'open3' + +# since we need the xml-simple gem, and we want this script self-contained, let's grab it just when we need it +begin + gem "xml-simple" + rescue LoadError + system("gem install --user-install xml-simple") + Gem.clear_paths +end + +require 'xmlsimple' + +# add custom Lint jar +if !CUSTOM_LINT_FILE.nil? && + CUSTOM_LINT_FILE.length > 0 + + ENV["ANDROID_LINT_JARS"] = Dir.pwd + "/" + CUSTOM_LINT_FILE + puts "adding custom lint rules to default set: " + puts ENV["ANDROID_LINT_JARS"] +end + +# run Lint +puts "running Lint..." +system './gradlew clean lintGplayDebug 1>/dev/null' + +# confirm that Lint ran w/out error +result = $?.to_i +if result != 0 + puts "FAIL: failed to run ./gradlew clean lintGplayDebug" + exit 1 +end + +# find Lint report file +lint_reports = Dir.glob(LINT_REPORT_FILE) +if lint_reports.length == 0 + puts "Lint HTML report not found." + exit 1 +end +lint_report = String.new(lint_reports[0]) + +# find error/warning count string in HTML report +error_warning_string = "" +File.open lint_report do |file| + error_warning_string = file.find { |line| line =~ /([0-9]* error[s]? and )?[0-9]* warning[s]?/ } +end + +# find number of errors +error_string = error_warning_string.match(/[0-9]* error[s]?/) + +if (error_string.nil?) + current_error_count = 0 +else + current_error_count = error_string[0].match(/[0-9]*/)[0].to_i +end + +puts "found errors: " + current_error_count.to_s + +# find number of warnings +if CHECK_WARNINGS == true + warning_string = error_warning_string.match(/[0-9]* warning[s]?/)[0] + current_warning_count = warning_string.match(/[0-9]*/)[0].to_i + puts "found warnings: " + current_warning_count.to_s +end + +# get previous error and warning counts from last successful build + +previous_results = false + +previous_lint_reports = Dir.glob(PREVIOUS_LINT_RESULTS_FILE) +if previous_lint_reports.nil? || + previous_lint_reports.length == 0 + + previous_lint_report = File.new(PREVIOUS_LINT_RESULTS_FILE, "w") # create for writing to later +else + previous_lint_report = String.new(previous_lint_reports[0]) + + previous_error_warning_string = "" + File.open previous_lint_report do |file| + previous_error_warning_string = file.find { |line| line =~ /([0-9]* error[s]? and )?[0-9]* warning[s]?/ } + end + + unless previous_error_warning_string.nil? + previous_results = true + + previous_error_string = previous_error_warning_string.match(/[0-9]* error[s]?/) + if previous_error_string.nil? + previous_error_string = "0 errors" + else + previous_error_string = previous_error_string[0] + end + previous_error_count = previous_error_string.match(/[0-9]*/)[0].to_i + puts "previous errors: " + previous_error_count.to_s + + if CHECK_WARNINGS == true + previous_warning_string = previous_error_warning_string.match(/[0-9]* warning[s]?/) + if previous_warning_string.nil? + previous_warning_string = "0 warnings" + else + previous_warning_string = previous_warning_string[0] + end + previous_warning_count = previous_warning_string.match(/[0-9]*/)[0].to_i + puts "previous warnings: " + previous_warning_count.to_s + end + end +end + +# compare previous error count with current error count +if previous_results == true && + current_error_count > previous_error_count + puts "FAIL: error count increased" + exit 1 +end + +# compare previous warning count with current warning count +if CHECK_WARNINGS == true && + previous_results == true && + current_warning_count > previous_warning_count + + puts "FAIL: warning count increased" + exit 1 +end + +# check if warning and error count stayed the same +if previous_results == true && + current_error_count == previous_error_count && + current_warning_count == previous_warning_count + + puts "SUCCESS: count stayed the same" + exit 2 +end + +# either error count or warning count DECREASED + +# write new results to file (will overwrite existing, or create new) +File.write(previous_lint_report, "DO NOT TOUCH; GENERATED BY DRONE\n" + error_warning_string) + +# update git user name and email for this script +system ("git config --local user.name 'github-actions'") +system ("git config --local user.email 'github-actions@github.com'") + +# add previous Lint result file to git +system ('git add ' + PREVIOUS_LINT_RESULTS_FILE) + +# commit changes +system('git commit -sm "Analysis: update lint results to reflect reduced error/warning count"') + +# push to origin +system ('git push') + +puts "SUCCESS: count was reduced" +exit 0 # success diff --git a/scripts/analysis/spotbugs-up.rb b/scripts/analysis/spotbugs-up.rb new file mode 100755 index 0000000..d7582d2 --- /dev/null +++ b/scripts/analysis/spotbugs-up.rb @@ -0,0 +1,53 @@ +## Script originally from https://github.com/tir38/android-lint-entropy-reducer at 07.05.2017 +# heavily modified since then + +# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2017 Jason Atwood +# SPDX-FileCopyrightText: 2017 Tobias Kaminsky +# SPDX-License-Identifier: GPL-3.0-or-later + +Encoding.default_external = Encoding::UTF_8 +Encoding.default_internal = Encoding::UTF_8 + +puts "=================== starting Android Spotbugs Entropy Reducer ====================" + +# get args +base_branch = ARGV[0] + +require 'fileutils' +require 'pathname' +require 'open3' + +# run Spotbugs +puts "running Spotbugs..." +system './gradlew spotbugsGplayDebug' + +# find number of warnings +current_warning_count = `./scripts/analysis/spotbugsSummary.py --total`.to_i +puts "found warnings: " + current_warning_count.to_s + +# get warning counts from target branch +previous_xml = "/tmp/#{base_branch}.xml" +previous_results = File.file?(previous_xml) + +if previous_results == true + previous_warning_count = `./scripts/analysis/spotbugsSummary.py --total --file #{previous_xml}`.to_i + puts "previous warnings: " + previous_warning_count.to_s +end + +# compare previous warning count with current warning count +if previous_results == true && current_warning_count > previous_warning_count + puts "FAIL: warning count increased" + exit 1 +end + +# check if warning and error count stayed the same +if previous_results == true && current_warning_count == previous_warning_count + puts "SUCCESS: count stayed the same" + exit 0 +end + +# warning count DECREASED +if previous_results == true && current_warning_count < previous_warning_count + puts "SUCCESS: count decreased from " + previous_warning_count.to_s + " to " + current_warning_count.to_s +end diff --git a/scripts/analysis/spotbugsComparison.py b/scripts/analysis/spotbugsComparison.py new file mode 100755 index 0000000..4e32f1f --- /dev/null +++ b/scripts/analysis/spotbugsComparison.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2017 Tobias Kaminsky +# SPDX-License-Identifier: GPL-3.0-or-later +import argparse +import defusedxml.ElementTree as ET + +import spotbugsSummary + + +def print_comparison(old: dict, new: dict, link_base: str, link_new: str): + all_keys = sorted(set(list(old.keys()) + list(new.keys()))) + + output = "" + old_header = f"Base" if link_base is not None else "Base" + output += f"" + new_header = f"New" if link_new is not None else "New" + output += f"" + output += "" + + for category in all_keys: + category_count_old = old[category] if category in old else 0 + category_count_new = new[category] if category in new else 0 + new_str = f"{category_count_new}" if category_count_new != category_count_old else str(category_count_new) + output += "" + output += f"" + output += f"" + output += f"" + output += "" + + output += "" + output += "" + output += f"" + output += f"" + output += "" + + output += "
Category{old_header}{new_header}
{category}{category_count_old}{new_str}
Total{sum(old.values())}{sum(new.values())}
" + + print(output) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("base_file", help="base file for comparison") + parser.add_argument("new_file", help="new file for comparison") + parser.add_argument("--link-base", help="http link to base html report") + parser.add_argument("--link-new", help="http link to new html report") + args = parser.parse_args() + + base_tree = ET.parse(args.base_file) + base_summary = spotbugsSummary.get_counts(base_tree) + + new_tree = ET.parse(args.new_file) + new_summary = spotbugsSummary.get_counts(new_tree) + + print_comparison(base_summary, new_summary, args.link_base, args.link_new) diff --git a/scripts/analysis/spotbugsSummary.py b/scripts/analysis/spotbugsSummary.py new file mode 100755 index 0000000..3ee9b99 --- /dev/null +++ b/scripts/analysis/spotbugsSummary.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2017 Tobias Kaminsky +# SPDX-License-Identifier: GPL-3.0-or-later +import argparse +import defusedxml.ElementTree as ET + + +def get_counts(tree): + category_counts = {} + category_names = {} + for child in tree.getroot(): + if child.tag == "BugInstance": + category = child.attrib['category'] + if category in category_counts: + category_counts[category] = category_counts[category] + 1 + else: + category_counts[category] = 1 + elif child.tag == "BugCategory": + category = child.attrib['category'] + category_names[category] = child[0].text + + summary = {} + for category in category_counts.keys(): + summary[category_names[category]] = category_counts[category] + return summary + + +def print_html(summary): + output = "" + + categories = sorted(summary.keys()) + for category in categories: + output += "" + output += f"" + output += f"" + output += "" + + output += "" + output += "" + output += f"" + output += "" + + output += "
CategoryCount
{category}{summary[category]}
Total{sum(summary.values())}
" + + print(output) + + +def print_total(summary): + print(sum(summary.values())) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--total", help="print total count instead of summary HTML", + action="store_true") + parser.add_argument("--file", help="file to parse", default="app/build/reports/spotbugs/gplayDebug.xml") + args = parser.parse_args() + tree = ET.parse(args.file) + summary = get_counts(tree) + if args.total: + print_total(summary) + else: + print_html(summary) diff --git a/scripts/checkGplayLimitation.sh b/scripts/checkGplayLimitation.sh new file mode 100755 index 0000000..74ca11f --- /dev/null +++ b/scripts/checkGplayLimitation.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2021 Andy Scherzinger +# SPDX-License-Identifier: GPL-3.0-or-later +# + +result="" + +for log in fastlane/metadata/android/*/changelogs/* + do + if [[ -e $log && $(wc -m $log | cut -d" " -f1) -gt 500 ]] + then + result=$log"
"$result + fi +done + +echo -e "$result"; diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100755 index 0000000..5ab3572 --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,19 @@ +#!/bin/bash +# Pre-commit hook: don't allow commits if detekt or ktlint fail. Skip with "git commit --no-verify". + +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2021 Álvaro Brey Vilas +# SPDX-License-Identifier: GPL-3.0-or-later + +echo "Running pre-commit checks..." + +if ! ./gradlew --daemon ktlintCheck &>/dev/null; then + echo >&2 "ktlint failed! Run ./gradlew ktlintCheck for details" + echo >&2 "Hint: fix most lint errors with ./gradlew ktlintFormat" + exit 1 +fi + +if ! ./gradlew --daemon detekt &>/dev/null; then + echo >&2 "Detekt failed! See report at file://$(pwd)/app/build/reports/detekt/detekt.html" + exit 1 +fi diff --git a/scripts/hooks/pre-push b/scripts/hooks/pre-push new file mode 100755 index 0000000..91b9ed7 --- /dev/null +++ b/scripts/hooks/pre-push @@ -0,0 +1,32 @@ +#!/bin/bash +# Pre-push: Don't allow commits without Signed-off-by. Skip with "git push --no-verify". + +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2021 Álvaro Brey Vilas +# SPDX-License-Identifier: GPL-3.0-or-later + +set -euo pipefail + +z40=0000000000000000000000000000000000000000 # magic deleted ref + +while read local_ref local_sha remote_ref remote_sha; do + if [ "$local_sha" != $z40 ]; then + if [ "$remote_sha" = $z40 ]; then + # New branch, examine all commits + range="$(git merge-base master $local_sha)..$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for commits without sign-off + commit=$(git rev-list --no-merges --grep 'Signed-off-by' --invert-grep "$range") + if [ -n "$commit" ]; then + echo >&2 "Found commits without sign-off in $local_ref. Aborting push. Offending commits:" + echo >&2 "$commit" + exit 1 + fi + fi +done + +exit 0 diff --git a/scripts/lib.sh b/scripts/lib.sh new file mode 100755 index 0000000..98dcff0 --- /dev/null +++ b/scripts/lib.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# +# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2022 Álvaro Brey +# SPDX-License-Identifier: MIT +# + +## This file is intended to be sourced by other scripts + + +function err() { + echo >&2 "$@" +} + + +function curl_gh() { + if [[ -n "$GITHUB_TOKEN" ]] + then + curl \ + --silent \ + --header "Authorization: token $GITHUB_TOKEN" \ + "$@" + else + err "WARNING: No GITHUB_TOKEN found. Skipping API call" + fi + +} diff --git a/scripts/metadata/generate_metadata.py b/scripts/metadata/generate_metadata.py new file mode 100644 index 0000000..3c6b5d6 --- /dev/null +++ b/scripts/metadata/generate_metadata.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +# Author: Torsten Grote +# License: GPLv3 or later +# copied on 2017/11/06 from https://github.com/grote/Transportr/blob/master/fastlane/generate_metadata.py +# adapted by Tobias Kaminsky and then again by Mario Danic + +# SPDX-FileCopyrightText: 2017 Torsten Grote +# SPDX-FileCopyrightText: 2017-2018 Tobias Kaminsky +# SPDX-FileCopyrightText: 2017-2018 Mario Danic +# SPDX-License-Identifier: GPL-3.0-or-later + +import codecs +import os +import shutil +from xml.etree import ElementTree + +XML_PATH = '../../app/src/main/res' +METADATA_PATH = '../../fastlane/metadata/android/' +DEFAULT_LANG = 'en-US' +LANG_MAP = { + 'values': 'en-US', + 'values-en-rGB': 'en-GB', + 'values-ca': 'ca', + 'values-cs': 'cs-CZ', + 'values-de': 'de-DE', + 'values-es': 'es-ES', + 'values-fr': 'fr-FR', + 'values-hu': 'hu-HU', + 'values-it': 'it-IT', + 'values-pt-rBR': 'pt-BR', + 'values-pt-rPT': 'pt-PT', + 'values-ta': 'ta-IN', + 'values-sv': 'sv-SE', + 'values-sq-rAL': 'sq-AL', + 'values-sq-rMK': 'sq-MK', + 'values-iw-rIL': 'iw-IL', + 'values-ar': 'ar-AR', + 'values-bg-rBG': 'bg-BG', + 'values-da': 'da-DK', + 'values-fi-rFI': 'fi-FI', + 'values-gl-rES': 'gl-ES', + 'values-tr': 'tr-TR', + 'values-uk': 'uk-UK', + 'values-vi': 'vi-VI', + 'values-ro': 'ro-RO', + 'values-ru': 'ru-RU', + 'values-sr': 'sr-SR', + 'values-pl': 'pl-PL', + 'values-el': 'el-GR', + 'values-ko': 'ko-KR', + 'values-nl': 'nl-NL', + 'values-ja': 'ja-JP', + 'values-no-rNO': 'no-NO', + 'values-eu': 'eu-ES', + 'values-lt-rLT': 'lt-LT', + 'values-zh-rKN': 'zh-HK', + 'values-zk': 'zk-CN', + 'values-is': 'is-IS', + 'values-id': 'id-ID', + 'values-cs-rCZ': 'cs-CZ', + 'values-sl': 'sl-SL', + 'values-fa': 'fa-FA' +} + +PATH = os.path.dirname(os.path.realpath(__file__)) + + +def main(): + path = os.path.join(PATH, XML_PATH) + for entry in os.listdir(path): + directory = os.path.join(path, entry) + if not os.path.isdir(directory) or entry not in LANG_MAP.keys(): + continue + strings_file = os.path.join(directory, 'strings.xml') + if not os.path.isfile(strings_file): + print("Error: %s does not exist" % strings_file) + continue + + print() + print(LANG_MAP[entry]) + print("Parsing %s..." % strings_file) + e = ElementTree.parse(strings_file).getroot() + short_desc = e.find('.//string[@name="nc_store_short_desc"]') + full_desc = e.find('.//string[@name="nc_store_full_desc"]') + if short_desc is not None: + save_file(short_desc.text, LANG_MAP[entry], 'short_description.txt') + if full_desc is not None: + save_file(full_desc.text, LANG_MAP[entry], 'full_description.txt') + + +def save_file(text, directory, filename): + directory_path = os.path.join(PATH, METADATA_PATH, directory) + + if not os.path.exists(directory_path): + os.makedirs(directory_path) + if filename == 'short_description.txt': + limit = 80 + else: + limit = 0 + text = clean_text(text, limit) + check_title(directory_path) + file_path = os.path.join(directory_path, filename) + print("Writing %s..." % file_path) + with codecs.open(file_path, 'w', 'utf-8') as f: + f.write(text) + f.write("\n") + + +def clean_text(text, limit=0): + text = text.replace('\\\'', '\'').replace('\\n', '\n') + if limit != 0 and len(text) > limit: + print("Warning: Short Description longer than 80 characters, truncating...") + text = text[:limit] + return text + + +def check_title(directory): + title_path = os.path.join(directory, 'title.txt') + if os.path.exists(title_path): + return + default_title_path = os.path.join(directory, '..', DEFAULT_LANG, 'title.txt') + shutil.copy(default_title_path, title_path) + + +if __name__ == "__main__": + main() diff --git a/scripts/repo b/scripts/repo new file mode 100644 index 0000000..c1cc361 --- /dev/null +++ b/scripts/repo @@ -0,0 +1 @@ +talk-android diff --git a/scripts/repo.license b/scripts/repo.license new file mode 100644 index 0000000..23e2d6b --- /dev/null +++ b/scripts/repo.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/scripts/tools/OxygenConversion.MD b/scripts/tools/OxygenConversion.MD new file mode 100644 index 0000000..78f36e9 --- /dev/null +++ b/scripts/tools/OxygenConversion.MD @@ -0,0 +1,11 @@ + +brew install coreutils +find * -type l -exec bash -c 'ln -f "$(greadlink -m "$0")" "$0"' {} \; +rename 's/-/_/g' * +rename 's/\+/_/g' * +rename 's/\./_/g' * +rename 's/_png/\.png/g' * + diff --git a/scripts/wait_for_emulator.sh b/scripts/wait_for_emulator.sh new file mode 100755 index 0000000..19ff9ee --- /dev/null +++ b/scripts/wait_for_emulator.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# +# SPDX-FileCopyrightText: 2021 Ralf Kistner +# SPDX-License-Identifier: CC0-1.0 +# + +# Originally written by Ralf Kistner , but placed in the public domain + +bootanim="" +failcounter=0 +checkcounter=0 + +until [[ "$bootanim" =~ "stopped" ]]; do + bootanim=`adb -e shell getprop init.svc.bootanim 2>&1` + echo "($checkcounter) $bootanim" + if [[ "$bootanim" =~ "not found" || "$bootanim" =~ "error" ]]; then + let "failcounter += 1" + if [[ $failcounter -gt 3 ]]; then + echo "Failed to start emulator" + exit 1 + fi + fi + let "checkcounter += 1" + sleep 20 +done +echo "($checkcounter) Done" +adb -e shell input keyevent 82 +echo "($checkcounter) Unlocked emulator screen" \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..b3d00a2 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,22 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2022 Tim Krüger + * SPDX-License-Identifier: GPL-3.0-or-later + */ +include ':app' + +//// uncomment to use local build of common-ui +//includeBuild('../android-common') { +// dependencySubstitution { +// substitute module('com.github.nextcloud.android-common:ui') using project(':ui') +// } +//} + +//includeBuild('../../../deps/ImagePicker') { +// dependencySubstitution { +// substitute module('com.github.nextcloud-deps:ImagePicker') using project(':imagepicker') +// } +//} diff --git a/spotbugs-filter.xml b/spotbugs-filter.xml new file mode 100644 index 0000000..4bbfcf3 --- /dev/null +++ b/spotbugs-filter.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

GkyA$;bM=8 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 z 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/raw/librem_by_feandesign_message.ogg b/app/src/main/res/raw/librem_by_feandesign_message.ogg new file mode 100644 index 0000000000000000000000000000000000000000..567f9ce949e0c7746d757b1ee4c0abc34d5e9dcf GIT binary patch literal 82206 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